---
title: Vision OCR
url: https://blog.guigpap.com/fr/workflows/vision-ocr/
url_md: https://blog.guigpap.com/fr/workflows/vision-ocr.md
category: automation
date: '2026-05-04'
maturite: production
techno:
- n8n
- claude
application:
- ai
- content
---
# Vision OCR
> Sous-workflow de classification et extraction structurée d'images via Gemini Vision et cli-ollama
## 1. Quoi ? — Définition et contexte
**Vision OCR** est un sous-workflow N8N appelé depuis le [Telegram Orchestrator](/fr/workflows/telegram-orchestrator/) chaque fois qu'un utilisateur envoie une photo. Il classe l'image dans une des cinq catégories de documents reconnus, puis applique un schéma d'extraction spécialisé pour retourner des données structurées prêtes à être consommées par les workflows métier (Odoo, notes, factures).
> **Note - Sub-workflow de pure extraction**
>
> Vision OCR ne parle pas à Telegram, n'écrit pas dans Odoo, ne notifie personne. Il reçoit une image en entrée, retourne un objet `{status, docType, extracted, text}` en sortie. C'est un composant de plomberie — la décision de quoi faire avec l'extraction revient au workflow appelant.
### Métadonnées
| Champ | Valeur |
|-------|--------|
| **Workflow ID** | `2ZDgU3TWbF4OeOKY` |
| **Type** | Execute Workflow Trigger (passthrough) |
| **Nodes** | 14 |
| **Appelé par** | Binary Content Handler (`QtkJDN8XAGlpcSPV`) |
| **Modèle** | `gemini-flash-yolo` via cli-ollama |
| **Issue** | #176 |
### Architecture visuelle
```mermaid
flowchart TD
subgraph Input["Entrée · passthrough Binary Content Handler"]
direction TB
Photo["Photo Telegram · base64 + mimeType"]
end
subgraph Detect["Phase 1 · Classification"]
direction TB
HTTP1["HTTP cli-ollama gemini-flash-yolo"]
Parse["Parse Detection · fallback regex"]
SwitchType["Switch Doc Type"]
end
subgraph Extract["Phase 2 · Extraction par schéma"]
direction TB
BC["Extract Business Card"]
INV["Extract Invoice"]
SC["Extract Screenshot"]
HW["Extract Handwritten"]
GEN["Extract General Document"]
end
subgraph Output["Sortie · contrat uniforme"]
direction TB
FormatResult["Parse and Format · escape HTML"]
Success["status: success"]
Fallback["status: fallback · not_document"]
ErrOut["status: error"]
end
Input --> HTTP1 --> Parse
Parse -->|HTTP error| ErrOut
Parse -->|not_document| Fallback
Parse -->|recognized type| SwitchType
SwitchType --> BC
SwitchType --> INV
SwitchType --> SC
SwitchType --> HW
SwitchType --> GEN
BC --> FormatResult
INV --> FormatResult
SC --> FormatResult
HW --> FormatResult
GEN --> FormatResult
FormatResult --> Success
```
### Les 6 types de documents
| Type | Champs extraits | Usage typique |
|------|-----------------|---------------|
| `business_card` | name, function, company_name, email, phone, mobile, street, city, zip, country, website, comment | Création de contact Odoo |
| `invoice` | vendor, invoice_number, date, items[], subtotal, tax, total, currency | Pièce comptable Odoo |
| `screenshot` | visible_text, ui_elements[], error_messages[], context | Discussion conversationnelle, tickets |
| `handwritten_note` | transcribed_text, confidence, language | Note dans Obsidian vault |
| `general_document` | full_text, document_title, key_sections[] | Texte recherche-able |
| `not_document` | (aucun) | Photo de scène, fallback IA Router |
---
## 2. Pourquoi ? — Enjeux et motivations
### Le problème sans Vision OCR
Avant ce sous-workflow, chaque photo envoyée au bot Telegram était traitée comme un message vide ou nécessitait une saisie manuelle. Photographier une carte de visite et taper ensuite tous les champs dans Odoo prenait plusieurs minutes par contact.
| Problème sans extraction | Conséquence |
|---------------------------|-------------|
| **Saisie manuelle** | Quelques minutes par carte, taux d'erreur élevé |
| **Pas de structure** | Impossible de filtrer/rechercher après coup |
| **Pas de classification** | Toutes les photos traitées identiquement |
| **Pas de fallback** | Une photo de paysage générait une erreur |
### Pourquoi un seul prompt ne suffit pas
Demander à un modèle vision *"extrait les informations utiles"* sur n'importe quelle image produit des résultats inégaux. Une carte de visite a des champs prévisibles, une facture a un format différent, un screenshot d'erreur n'a pas de "champs". Le pipeline en deux phases résout cette tension :
| Approche | Avantage | Inconvénient |
|----------|----------|--------------|
| **Prompt générique unique** | Simple, 1 appel LLM | Champs incohérents, format JSON instable |
| **Pipeline classify → extract** | Schéma adapté à chaque type | 2 appels LLM, latence x2 |
L'overhead de latence (≈ 4-6 s) est acceptable parce que l'utilisateur attend déjà la transcription. La qualité de l'extraction et la stabilité du contrat de sortie justifient le coût.
### Pourquoi Gemini Flash plutôt que GPT-4 Vision
| Critère | Gemini Flash | GPT-4 Vision | Claude 3 Vision |
|---------|--------------|--------------|-----------------|
| **Coût** | Gratuit (cli-ollama) | $0.01 / image | $0.005 / image |
| **Latence** | 1-3 s | 3-8 s | 2-5 s |
| **JSON structuré** | Variable, parsing défensif requis | Stable | Stable |
| **OCR latin** | Excellent | Excellent | Excellent |
| **OCR manuscrit** | Bon | Excellent | Très bon |
| **Hébergement** | Self-hosted (cli-ollama) | API OpenAI | API Anthropic |
Le choix Gemini Flash s'aligne avec la stratégie multi-provider de cli-ollama : pas de coût à l'image, latence acceptable, JSON parseable avec un fallback regex défensif.
---
## 3. Comment ? — Mise en œuvre technique
### Le contrat de sortie
Trois statuts possibles, tous renvoyés via le même format :
**Succès :**
```json
{
"status": "success",
"docType": "invoice",
"extracted": {
"vendor": "Amazon",
"invoice_number": "INV-2026-001",
"total": 23.98,
"currency": "EUR"
},
"text": "Facture\n\nVendeur: Amazon\n..."
}
```
**Fallback (pas un document) :**
```json
{
"status": "fallback",
"docType": "not_document"
}
```
**Erreur (cli-ollama injoignable) :**
```json
{
"status": "error",
"error": "cli-ollama request failed (HTTP 500)"
}
```
Le champ `text` contient une représentation HTML formatée prête à envoyer dans Telegram. Le champ `extracted` contient les données structurées qu'un workflow appelant peut consommer pour créer un enregistrement Odoo, par exemple.
### Stratégie de fallback
Le parsing JSON Gemini est défensif sur plusieurs niveaux :
| Cas | Comportement |
|-----|--------------|
| **JSON propre** | Parse direct |
| **JSON enveloppé dans backticks markdown** | Strip ` ``` ` puis parse |
| **JSON avec préambule textuel** | Regex `\{[\s\S]*\}` puis parse |
| **JSON malformé** | Fallback sur `general_document` avec `text: ` |
| **Type inconnu retourné** | Force `not_document` |
| **HTTP error cli-ollama** | `status: error` propagé sans crash |
Cette défensive est nécessaire parce que Gemini Flash retourne parfois du texte additionnel ("Voici le résultat:") avant le JSON, ou enveloppe le JSON dans des backticks. Sans le fallback, le workflow appelant recevait des erreurs de parsing au lieu d'un fallback gracieux.
### Prompts par type
Le détecteur initial classifie l'image avec un prompt court :
```
Analyze this image and classify it as ONE of:
- business_card
- invoice
- screenshot
- handwritten_note
- general_document
- not_document
Return ONLY JSON: {"type": "...", "confidence": 0.95}
```
Puis le routage Switch envoie l'image vers un prompt d'extraction spécialisé. Exemple pour une carte de visite — les champs sont alignés sur le modèle `res.partner` d'Odoo pour permettre une création directe via XML-RPC :
```
Extract contact information from this business card.
Return ONLY JSON (null if not found):
{
"name": "Full name",
"function": "Job title",
"company_name": "Company",
"email": "...",
"phone": "...",
"mobile": "...",
"street": "...",
"city": "...",
"zip": "...",
"country": "...",
"website": "...",
"comment": "LinkedIn or notes"
}
```
> **Tip - Préfixage cli-ollama -yolo**
>
> Le modèle utilisé est `gemini-flash-yolo` (et non `gemini-flash`). Le suffixe `-yolo` signifie "auto-approve dangereux", ce qui désactive le plan-mode interactif du CLI Ollama et évite un timeout sur le webhook N8N (limite 120 s). Voir [AI Stack](/fr/infrastructure/ai-stack/) pour le détail.
### Intégration avec le Telegram Orchestrator
Le Binary Content Handler appelle Vision OCR via Execute Workflow puis route le résultat selon le `status` :
| Status | Routing |
|--------|---------|
| `success` + `docType=business_card` | Proposition d'enregistrer comme contact Odoo (boutons Telegram) |
| `success` + `docType=invoice` | Proposition de stocker comme pièce comptable |
| `success` + `docType=screenshot/handwritten/general` | Affichage direct + bouton "Discuter" (lance une conversation) |
| `fallback` (`not_document`) | Bascule vers IA Router pour traitement conversationnel |
| `error` | Notification d'erreur via [Notification Hub](/fr/workflows/notification-hub/) |
### Performance
| Étape | Latence typique |
|-------|-----------------|
| Encodage base64 + transfert | 100-500 ms (selon taille photo) |
| Détection classification | 1-2 s |
| Extraction par schéma | 2-4 s |
| Format HTML + return | < 100 ms |
| **Total bout-en-bout** | **3-7 s** |
La compression Telegram réduit déjà les photos à ~1 MB max, ce qui maintient la latence dans une fourchette acceptable même pour des cartes de visite haute résolution.
---
## 4. Et si ? — Perspectives et limites
### Limites actuelles
| Limite | Impact | Mitigation |
|--------|--------|------------|
| **Latence x2** | 2 appels LLM successifs | Acceptable, l'utilisateur attend déjà |
| **Pas de classification multi-doc** | Une photo avec carte + ticket = 1 seul type | Demander 2 photos séparées |
| **Pas de validation Odoo** | Email malformé créerait un partner invalide | Validation côté workflow appelant |
| **Pas de mémoire entre photos** | Chaque photo est traitée isolément | Le système conversationnel garde le contexte une fois la photo extraite |
### Scénarios d'évolution
**Si la qualité d'extraction se dégrade** :
- Passer à `gemini-pro-yolo` pour les types complexes (factures multi-lignes)
- Ajouter une étape de validation/correction par un second prompt
- Comparer avec un modèle alternatif (Claude Vision via Anthropic API si quota disponible)
**Si de nouveaux types de documents émergent** :
- Ajouter un nouveau case dans le Switch + un prompt d'extraction dédié
- Garder le contrat `{status, docType, extracted, text}` inchangé pour ne pas casser les workflows appelants
- Exemples candidats : `id_card`, `passport`, `recipe`, `prescription`
**Si les volumes augmentent significativement** :
- Mutualiser le cache de classification (mêmes images à 1-2 jours d'écart)
- Passer à un modèle Vision local quantisé (LLaVA, MiniCPM) hébergé sur le VPS pour zéro latence réseau
- Batching de plusieurs images en un seul appel API
---
## Pages liées
### Infrastructure
- [AI Stack](/fr/infrastructure/ai-stack/) — cli-ollama et le routing Gemini/Claude
- [N8N en mode Queue](/fr/infrastructure/n8n-queue-mode/) — Backend qui exécute le sous-workflow
### Workflows
- [Telegram Orchestrator](/fr/workflows/telegram-orchestrator/) — Binary Content Handler appelant
- [Système conversationnel](/fr/workflows/systeme-conversationnel/) — Suite logique après extraction d'un screenshot
- [Voice Transcription](/fr/workflows/voice-transcription/) — Pipeline analogue pour l'audio
### Référence
- [Glossaire](/fr/reference/glossary/) — OCR, Vision, Sub-workflow, cli-ollama
## Metadonnees agent
- Cet article est issu du blog GuiGPaP Lab.
- Contexte global du blog: https://blog.guigpap.com/llms.txt
- Contact auteur: https://odoo.guigpap.com/mon-cv
- Licence: CC-BY-SA 4.0