diff --git a/.opencode/ROADMAP-FONCTIONNALITES.md b/.opencode/ROADMAP-FONCTIONNALITES.md
new file mode 100644
index 0000000..8ce0b04
--- /dev/null
+++ b/.opencode/ROADMAP-FONCTIONNALITES.md
@@ -0,0 +1,175 @@
+# EFFI - ROADMAP FONCTIONNALITÉS
+
+**Date de création** : 6 Avril 2026
+**Dernière mise à jour** : 6 Avril 2026
+**Statut** : En attente d'implémentation
+
+---
+
+## 🔥 PRIORITÉ HAUTE
+
+### 1. OCR / Tesseract
+**Objectif** : Permettre à Effi de lire et extraire le texte des images (photos de livres, documents, etc.)
+
+**Use case** : L'ami de Folzo prend des photos de livres et veut les numériser
+
+**Implémentation** :
+- Installer Tesseract sur folzo-Corp : `sudo dnf install tesseract tesseract-langpack-fra`
+- Créer un endpoint API FastAPI pour l'OCR
+- Intégrer dans OpenWebUI comme Tool
+
+**Ressources** :
+- Tesseract OCR (open source)
+- Python : `pytesseract` + `Pillow`
+
+---
+
+### 2. Joplin Integration
+**Objectif** : RAG (Retrieval-Augmented Generation) sur les notes Joplin existantes
+
+**Use case** : Poser des questions à Effi sur ses propres notes
+
+**Implémentation** :
+- Exporter/exposer les notes Joplin via API
+- Vectorisation et stockage dans Qdrant
+- Créer un Tool "search_my_notes"
+
+**Ressources** :
+- Joplin Web Clipper API
+- Joplin Data API
+
+---
+
+### 3. Web Scraping
+**Objectif** : Permettre aux agents de lire et résumer n'importe quelle URL
+
+**Use case** : "Effi, résume-moi cet article" → paste URL
+
+**Implémentation** :
+- Créer un Tool OpenWebUI
+- Librairies : `requests` + `beautifulsoup4` ou `readability-lxml`
+- Gestion du rate limiting
+- Extraction texte propre (sans pubs/nav)
+
+**Ressources** :
+- newspaper3k (extraction article)
+- trafilatura (alternative)
+
+---
+
+## ⚡ AMÉLIORATIONS
+
+### 4. Optimiser Web Search
+**Objectif** : Améliorer la qualité et quantité des résultats de recherche web
+
+**Actions** :
+- Augmenter le nombre de résultats (actuellement limité)
+- Tester SearXNG (moteur de méta-recherche auto-hébergé)
+- Configurer la région/langue (fr-FR)
+- Filtres par date/type de contenu
+
+**Ressources** :
+- SearXNG Docker
+- DuckDuckGo API
+
+---
+
+### 5. Home Assistant - Fix Latence
+**Objectif** : Résoudre les problèmes de latence dans l'intégration Home Assistant
+
+**Problème actuel** : Latence élevée lors des appels API
+
+**Actions** :
+- Diagnostiquer la cause (réseau ? configuration ?)
+- Optimiser les appels (batch ? cache ?)
+- Vérifier la connectivité entre effi-server et HA
+
+---
+
+### 6. Calendrier - Intégration CalDAV
+**Objectif** : Permettre à Effi de consulter et gérer le calendrier
+
+**Use case** : "Qu'est-ce que j'ai de prévu demain ?"
+
+**Implémentation** :
+- Intégration CalDAV (Nextcloud, Google, etc.)
+- Tool "check_calendar"
+- Tool "add_event"
+
+**Ressources** :
+- `caldav` Python library
+
+---
+
+## 💡 FONCTIONNALITÉS FUN
+
+### 7. Voice Commands
+**Objectif** : Activer/désactiver des services par la voix
+
+**Use case** : "Effi, lance ComfyUI" ou "Effi, éteins le serveur"
+
+**Implémentation** :
+- Mots-clés déclencheurs (hotwords)
+- Actions système via scripts
+- Intégration avec les commandes vocales existantes
+
+---
+
+### 8. Automation Scripts
+**Objectif** : Tâches planifiées et automatisées
+
+**Exemples** :
+- Backup automatique des conversations
+- Rapport quotidien (météo, calendrier, news)
+- Vérification santé des services
+- Nettoyage automatique des vieux fichiers
+
+**Implémentation** :
+- Cron jobs ou systemd timers
+- Scripts Python dans `/scripts/automation/`
+
+---
+
+### 9. More Models pour ComfyUI
+**Objectif** : Ajouter plus de variété de modèles pour la génération d'images
+
+**Catégories** :
+- **Art/Fantasy** : DreamShaper, EpicRealism
+- **Anime/Manga** : AnythingXL, CounterfeitXL
+- **Photorealistic** : RealVisXL, JuggernautXL (déjà présent)
+- **3D/Rendering** : PixarXL, DisneyStyle
+
+**Script existant** : `comfyui/download-more-models.sh`
+
+---
+
+## 📋 PROCHAINES SESSIONS
+
+### Session suggérée #1 : OCR/Tesseract
+1. Installation Tesseract
+2. Création API FastAPI
+3. Tool OpenWebUI
+4. Test avec photo de livre
+
+### Session suggérée #2 : Web Scraping
+1. Création du Tool
+2. Tests avec différents sites
+3. Intégration agents
+
+### Session suggérée #3 : Joplin Integration
+1. Configurer API Joplin
+2. Export notes vers Qdrant
+3. Tool search_my_notes
+
+---
+
+## 📝 NOTES
+
+- Prioriser l'OCR (besoin immédiat pour l'ami de Folzo)
+- Web Scraping utile pour tous les agents
+- Joplin demande plus de travail (vectorisation)
+- Les améliorations peuvent être faites en parallèle
+
+---
+
+*Roadmap créée par Folzo - 6 Avril 2026*
diff --git a/docs/WEB-SCRAPER-CONFIG.md b/docs/WEB-SCRAPER-CONFIG.md
new file mode 100644
index 0000000..b57dde9
--- /dev/null
+++ b/docs/WEB-SCRAPER-CONFIG.md
@@ -0,0 +1,111 @@
+# Configuration Web Search and Crawl (OpenWebUI)
+
+Guide de configuration du Tool **Web Search and Crawl** dans OpenWebUI avec Crawl4AI.
+
+## Prérequis
+
+- [ ] Crawl4AI installé et démarré sur effi-server (voir `install-on-effiserver.sh`)
+- [ ] OpenWebUI accessible sur http://192.168.1.27:3000
+
+## Étapes de configuration
+
+### 1. Vérifier que Crawl4AI tourne
+
+Sur **effi-server** :
+```bash
+curl http://localhost:11235/health
+# Doit retourner : {"status":"healthy"}
+```
+
+### 2. Accéder au Tool dans OpenWebUI
+
+1. Ouvrir http://192.168.1.27:3000
+2. Se connecter en admin
+3. Aller dans **Admin Panel** → **Tools**
+4. Trouver **Web Search and Crawl**
+5. Cliquer sur l'icône ⚙️ (Settings/Valves)
+
+### 3. Configurer les Valves
+
+| Valve | Valeur | Description |
+|-------|--------|-------------|
+| **Crawl4AI Base URL** | `http://localhost:11235` | URL du service Crawl4AI |
+| **Crawl4AI API Token** | (vide) | Token si configuré dans docker-compose |
+| **Timeout** | `30000` | Timeout en millisecondes (30s) |
+| **Max Length** | `10000` | Nombre max de caractères extraits |
+| **Enable JavaScript** | ✅ | Activer l'exécution JavaScript |
+
+### 4. Activer le Tool
+
+1. Cocher la case **Enable** sur le Tool
+2. Cliquer sur **Save**
+3. Le Tool est maintenant actif !
+
+## Test de fonctionnement
+
+Créer une nouvelle conversation et demander :
+
+```
+"Effi, cherche les dernières nouvelles sur Fedora 42 et résume-moi les articles"
+```
+
+L'agent devrait :
+1. Utiliser la recherche web
+2. Scraper les pages trouvées
+3. Te présenter un résumé
+
+## Troubleshooting
+
+### "Crawl4AI not responding"
+
+Vérifier sur effi-server :
+```bash
+docker ps | grep crawl4ai
+docker logs effi-crawl4ai
+```
+
+### "Timeout error"
+
+Augmenter la valve **Timeout** à `60000` (60 secondes) dans les settings du Tool.
+
+### "Memory error"
+
+La version CPU est limitée à 2GB. Pour les pages très lourdes :
+- Augmenter dans `docker-compose.yml` : `memory: 3G`
+- Redémarrer : `docker-compose up -d`
+
+## Commandes utiles
+
+**Redémarrer Crawl4AI** :
+```bash
+cd ~/effi-crawl4ai
+docker-compose restart
+```
+
+**Voir les logs** :
+```bash
+docker logs -f effi-crawl4ai
+```
+
+**Mettre à jour l'image** :
+```bash
+cd ~/effi-crawl4ai
+docker-compose pull
+docker-compose up -d
+```
+
+## Fonctionnalités disponibles
+
+Une fois configuré, tu peux demander à Effi :
+
+| Requête | Description |
+|---------|-------------|
+| "Cherche X et résume" | Recherche + résumé automatique |
+| "Extraire le contenu de [URL]" | Scraping direct d'une page |
+| "Comparer [produit] sur plusieurs sites" | Multi-site scraping |
+| "Vérifier les prix de [article]" | Surveillance de prix |
+| "Trouver des documentations sur [sujet]" | Recherche technique |
+
+---
+
+*Configuration créée le 6 Avril 2026*
diff --git a/docs/WEB-SCRAPER-TOOL.md b/docs/WEB-SCRAPER-TOOL.md
new file mode 100644
index 0000000..fce82ec
--- /dev/null
+++ b/docs/WEB-SCRAPER-TOOL.md
@@ -0,0 +1,188 @@
+# Tool Web Scraping pour OpenWebUI
+
+## Configuration
+
+Dans OpenWebUI (http://192.168.1.27:3000) :
+
+1. **Aller dans** : Admin → Tools → Create Tool
+2. **Nom** : Web Scraper
+3. **ID** : web_scraper
+4. **Description** : Extrait le contenu texte de n'importe quelle URL web
+
+## Code du Tool
+
+```python
+from typing import Optional
+import requests
+from urllib.parse import urlparse
+import re
+
+class Tools:
+ def __init__(self):
+ """Initialize Web Scraper Tool"""
+ self.timeout = 15
+ self.headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.0'
+ }
+
+ def scrape_url(self, url: str, max_chars: int = 5000) -> str:
+ """
+ Scrape le contenu texte d'une URL web
+
+ Args:
+ url: L'URL à scraper (doit commencer par http:// ou https://)
+ max_chars: Nombre maximum de caractères à retourner (défaut: 5000)
+
+ Returns:
+ Le contenu texte extrait de la page
+ """
+ try:
+ # Validation URL
+ if not url.startswith(('http://', 'https://')):
+ return "Erreur: L'URL doit commencer par http:// ou https://"
+
+ parsed = urlparse(url)
+ if not parsed.netloc:
+ return "Erreur: URL invalide"
+
+ # Télécharger la page
+ response = requests.get(url, headers=self.headers, timeout=self.timeout)
+ response.raise_for_status()
+
+ # Détecter l'encodage
+ if response.encoding == 'ISO-8859-1':
+ response.encoding = response.apparent_encoding
+
+ html = response.text
+
+ # Extraction simple du texte (sans BeautifulSoup pour garder le Tool léger)
+ # Supprimer les scripts et styles
+ text = re.sub(r'', '', html, flags=re.IGNORECASE | re.DOTALL)
+ text = re.sub(r'', '', text, flags=re.IGNORECASE | re.DOTALL)
+
+ # Supprimer les balises HTML
+ text = re.sub(r'<[^>]+>', ' ', text)
+
+ # Nettoyer le texte
+ text = re.sub(r'\s+', ' ', text) # Espaces multiples
+ text = text.strip()
+
+ # Décoder les entités HTML
+ text = text.replace('&', '&')
+ text = text.replace('<', '<')
+ text = text.replace('>', '>')
+ text = text.replace('"', '"')
+ text = text.replace(''', "'")
+ text = text.replace(' ', ' ')
+
+ # Tronquer si nécessaire
+ if len(text) > max_chars:
+ text = text[:max_chars] + f"\n\n[...] {len(text) - max_chars} caractères supplémentaires"
+
+ return f"Contenu extrait de {url}:\n\n{text}"
+
+ except requests.exceptions.Timeout:
+ return f"Erreur: Timeout après {self.timeout}s pour {url}"
+ except requests.exceptions.ConnectionError:
+ return f"Erreur: Impossible de se connecter à {url}"
+ except requests.exceptions.HTTPError as e:
+ return f"Erreur HTTP {e.response.status_code}: {url}"
+ except Exception as e:
+ return f"Erreur lors du scraping: {str(e)}"
+
+ def scrape_article(self, url: str) -> str:
+ """
+ Scrape un article en essayant d'extraire le contenu principal uniquement
+
+ Args:
+ url: L'URL de l'article
+
+ Returns:
+ Le contenu principal de l'article (titre + texte)
+ """
+ try:
+ if not url.startswith(('http://', 'https://')):
+ return "Erreur: L'URL doit commencer par http:// ou https://"
+
+ response = requests.get(url, headers=self.headers, timeout=self.timeout)
+ response.raise_for_status()
+
+ if response.encoding == 'ISO-8859-1':
+ response.encoding = response.apparent_encoding
+
+ html = response.text
+
+ # Extraire le titre
+ title_match = re.search(r'
]*>([^<]*)', html, re.IGNORECASE)
+ title = title_match.group(1).strip() if title_match else "Titre non trouvé"
+
+ # Essayer d'extraire le contenu article (heuristique simple)
+ # Chercher les balises article, main, ou div avec classe contenant "content"
+ article_patterns = [
+ r']*>(.*?)',
+ r']*>(.*?)',
+ r']*class=["\'][^"\']*(?:content|article|post)[^"\']*["\'][^>]*>(.*?)
',
+ ]
+
+ content = None
+ for pattern in article_patterns:
+ match = re.search(pattern, html, re.IGNORECASE | re.DOTALL)
+ if match:
+ content = match.group(1)
+ break
+
+ # Si pas trouvé, prendre tout le body
+ if not content:
+ body_match = re.search(r']*>(.*?)', html, re.IGNORECASE | re.DOTALL)
+ content = body_match.group(1) if body_match else html
+
+ # Nettoyer le contenu
+ content = re.sub(r'', '', content, flags=re.IGNORECASE | re.DOTALL)
+ content = re.sub(r'', '', content, flags=re.IGNORECASE | re.DOTALL)
+ content = re.sub(r'<[^>]+>', ' ', content)
+ content = re.sub(r'\s+', ' ', content).strip()
+
+ # Décoder les entités HTML
+ content = content.replace('&', '&').replace('<', '<').replace('>', '>')
+ content = content.replace('"', '"').replace(''', "'")
+
+ # Limiter la taille
+ if len(content) > 8000:
+ content = content[:8000] + "\n\n[...] Contenu tronqué"
+
+ return f"Titre: {title}\nURL: {url}\n\n{content}"
+
+ except Exception as e:
+ return f"Erreur: {str(e)}"
+```
+
+## Utilisation
+
+Une fois activé, les agents peuvent utiliser :
+
+- `scrape_url("https://example.com/article")` - Extraire tout le texte d'une page
+- `scrape_article("https://example.com/article")` - Extraire le contenu article uniquement
+
+## Exemples de prompts
+
+```
+"Effi, résume-moi cet article : https://..."
+→ L'agent utilise scrape_article automatiquement
+
+"Qu'est-ce qui est écrit sur cette page ? https://..."
+→ L'agent utilise scrape_url
+
+"Cherche des infos sur X et lis les 3 premiers liens"
+→ Web search + scrape_url pour chaque résultat
+```
+
+## Limitations
+
+- Extraction basique (pas de JavaScript)
+- Timeout 15 secondes
+- Sites avec protection anti-bot peuvent bloquer
+- Ne fonctionne pas avec les paywalls
+
+---
+
+*Tool créé le 6 Avril 2026*
diff --git a/effi-crawl4ai/Dockerfile.amd64 b/effi-crawl4ai/Dockerfile.amd64
new file mode 100644
index 0000000..3069f2b
--- /dev/null
+++ b/effi-crawl4ai/Dockerfile.amd64
@@ -0,0 +1,34 @@
+# Crawl4AI pour architecture AMD64 (x86_64)
+# Build local car l'image officielle est en ARM64
+
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Installer les dépendances système
+RUN apt-get update && apt-get install -y \
+ curl \
+ chromium \
+ chromium-driver \
+ && rm -rf /var/lib/apt/lists/*
+
+# Variables d'environnement
+ENV PYTHONUNBUFFERED=1
+ENV PLAYWRIGHT_BROWSERS_PATH=0
+ENV PIP_NO_CACHE_DIR=1
+
+# Installer Crawl4AI
+RUN pip install crawl4ai
+
+# Installer Playwright
+RUN pip install playwright && playwright install chromium
+
+# Port d'écoute
+EXPOSE 11235
+
+# Healthcheck
+HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
+ CMD curl -f http://localhost:11235/health || exit 1
+
+# Démarrer l'API
+CMD ["python", "-m", "crawl4ai.server", "--host", "0.0.0.0", "--port", "11235"]
diff --git a/effi-crawl4ai/Dockerfile.scraper b/effi-crawl4ai/Dockerfile.scraper
new file mode 100644
index 0000000..bc794d5
--- /dev/null
+++ b/effi-crawl4ai/Dockerfile.scraper
@@ -0,0 +1,20 @@
+FROM python:3.11-slim
+
+WORKDIR /app
+
+# Installer les dépendances
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4
+
+# Copier le code
+COPY scraper.py .
+
+EXPOSE 11235
+
+HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
+ CMD curl -f http://localhost:11235/health || exit 1
+
+CMD ["uvicorn", "scraper:app", "--host", "0.0.0.0", "--port", "11235"]
diff --git a/effi-crawl4ai/README-FOLZO-CORP.md b/effi-crawl4ai/README-FOLZO-CORP.md
new file mode 100644
index 0000000..b4a42f4
--- /dev/null
+++ b/effi-crawl4ai/README-FOLZO-CORP.md
@@ -0,0 +1,115 @@
+# Crawl4AI sur folzo-Corp
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ folzo-Corp (GPU) │
+│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐ │
+│ │ Ollama │ │ComfyUI │ │Effi-STT │ │Effi-OCR │ │Crawl4AI│ │
+│ │ :11434 │ │ :8188 │ │ :8200 │ │ :8300 │ │:11235 │ │
+│ └────┬────┘ └─────────┘ └─────────┘ └─────────┘ └───┬────┘ │
+│ │ │ │
+│ │ http://192.168.1.11:11235 │ │
+│ └──────────────────────────────────────────────────┘ │
+└─────────────────────────────────────────────────────────────────┘
+ │
+ │ HTTP API
+ │
+┌─────────────────────────────────────────────────────────────────┐
+│ effi-server (J5040) │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
+│ │ OpenWebUI │ │ Qdrant │ │ Tool Config │ │
+│ │ :3000 │ │ :6333 │ │ Crawl4AI Base URL: │ │
+│ └──────┬──────┘ └─────────────┘ │ http://192.168.1.11:11235│ │
+│ │ └─────────────────────────┘ │
+│ │ ❌ PAS de Crawl4AI local │
+└─────────┼───────────────────────────────────────────────────────┘
+ │
+ └──────► Réseau effi-net
+```
+
+## Déploiement
+
+### 1. Sur folzo-Corp (déjà fait)
+
+Les fichiers sont dans `/home/folzo/Documents/OpenCode/Projets/Effi/effi-crawl4ai/`
+
+```bash
+cd /home/folzo/Documents/OpenCode/Projets/Effi/effi-crawl4ai
+docker-compose up -d
+```
+
+### 2. Sur effi-server (à faire)
+
+**Supprimer le service local qui ne fonctionne pas :**
+
+```bash
+cd /root/effi
+
+# Option A : Script automatique
+curl -o remove-from-effi.sh [URL_du_script]
+bash remove-from-effi.sh
+
+# Option B : Manuel
+docker-compose stop crawl4ai
+docker-compose rm -f crawl4ai
+docker rmi unclecode/crawl4ai:basic
+
+# Éditer docker-compose.yml et supprimer le service crawl4ai (section #8)
+```
+
+**Ouvrir le firewall sur folzo-Corp** :
+
+```bash
+# Sur folzo-Corp
+sudo firewall-cmd --add-port=11235/tcp --permanent
+sudo firewall-cmd --reload
+```
+
+### 3. Configuration OpenWebUI
+
+Dans http://192.168.1.27:3000 :
+
+1. **Admin Panel** → **Tools** → **Web Search and Crawl** → ⚙️
+2. **Crawl4AI Base URL** : `http://192.168.1.11:11235` ← L'IP de folzo-Corp !
+3. **Timeout** : `30000` (ou `60000` si lenteur)
+4. Activer le Tool ✅
+
+## Avantages de cette architecture
+
+✅ **Crawl4AI** tourne sur CPU/GPU puissant (folzo-Corp)
+✅ **Effi-server** (J5040) n'est pas surchargé
+✅ **Réseau** local rapide entre les deux machines
+✅ **Charge répartie** : scraping sur folzo-Corp, interface sur effi-server
+
+## Vérification
+
+```bash
+# Sur folzo-Corp
+curl http://localhost:11235/health
+# Résultat : {"status":"healthy"}
+
+# Sur effi-server
+curl http://192.168.1.11:11235/health
+# Résultat : {"status":"healthy"}
+```
+
+## Test
+
+Dans OpenWebUI :
+> *"Effi, cherche les dernières nouvelles sur Linux et résume-moi un article"*
+
+## Troubleshooting
+
+**"Connection refused" depuis effi-server** :
+- Vérifier que Crawl4AI tourne sur folzo-Corp : `docker ps | grep crawl4ai`
+- Vérifier le firewall sur folzo-Corp : `sudo firewall-cmd --list-ports`
+
+**Timeout** :
+- Augmenter à 60000ms dans OpenWebUI
+- La latence réseau entre les machines peut causer des timeouts
+
+---
+
+*Architecture mise à jour pour J5040 + RX 7800 XT*
diff --git a/effi-crawl4ai/README-INTEGRATION.md b/effi-crawl4ai/README-INTEGRATION.md
new file mode 100644
index 0000000..521fd8f
--- /dev/null
+++ b/effi-crawl4ai/README-INTEGRATION.md
@@ -0,0 +1,128 @@
+# Crawl4AI - Intégration Effi Server
+
+Intégration du service **Crawl4AI** au docker-compose existant de `/root/effi/`.
+
+## Architecture
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ effi-server │
+│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
+│ │ OpenWebUI │ │ Qdrant │ │ Crawl4AI │ │
+│ │ :3000 │ │ :6333 │ │ :11235 │ │
+│ └──────┬──────┘ └─────────────┘ └──────┬──────┘ │
+│ │ │ │
+│ └──────────────────────────────────┘ │
+│ Réseau: effi-net │
+└─────────────────────────────────────────────────────────┘
+```
+
+## Installation
+
+### Option 1 : Script automatique (recommandé)
+
+Sur **effi-server**, dans `/root/effi/` :
+
+```bash
+cd /root/effi
+
+# Copier le script add-to-effi.sh ici
+# Puis exécuter :
+bash add-to-effi.sh
+```
+
+Le script va :
+1. Sauvegarder ton docker-compose.yml
+2. Ajouter le service Crawl4AI
+3. Démarrer le service
+4. Vérifier qu'il fonctionne
+
+### Option 2 : Manuel
+
+1. **Sauvegarder** le docker-compose existant :
+```bash
+cd /root/effi
+cp docker-compose.yml docker-compose.yml.backup
+```
+
+2. **Éditer** `docker-compose.yml` et ajouter avant la section `networks:` :
+
+```yaml
+ # ─────────────────────────────────────────────────────────
+ # 8. CRAWL4AI - Web Scraping pour OpenWebUI
+ # → http://localhost:11235 (accessible depuis OpenWebUI)
+ # ─────────────────────────────────────────────────────────
+ crawl4ai:
+ image: unclecode/crawl4ai:basic
+ container_name: crawl4ai
+ restart: unless-stopped
+ ports:
+ - "11235:11235"
+ environment:
+ - CRAWL4AI_API_TOKEN=${CRAWL4AI_API_TOKEN:-}
+ - MAX_CONCURRENT_TASKS=3
+ - CRAWL4AI_ENABLE_GPU=false
+ volumes:
+ - /dev/shm:/dev/shm
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ reservations:
+ memory: 512M
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
+ networks:
+ - effi-net
+```
+
+3. **Démarrer** le service :
+```bash
+docker-compose up -d crawl4ai
+```
+
+## Configuration OpenWebUI
+
+1. Aller dans **Admin Panel** → **Tools** → **Web Search and Crawl** → ⚙️
+2. **Crawl4AI Base URL** : `http://localhost:11235`
+3. **Timeout** : `30000`
+4. Activer le Tool ✅
+
+## Vérification
+
+```bash
+# Sur effi-server
+curl http://localhost:11235/health
+# Résultat attendu : {"status":"healthy"}
+```
+
+## Test
+
+Dans OpenWebUI :
+> *"Effi, cherche les dernières nouvelles sur Linux et résume-moi un article"*
+
+## Troubleshooting
+
+**Le service ne démarre pas** :
+```bash
+docker logs crawl4ai
+docker-compose ps
+```
+
+**Timeout** : Augmenter la valve à `60000` dans OpenWebUI
+
+**Mémoire** : Le service est limité à 2GB (suffisant pour la plupart des sites)
+
+## Fichiers
+
+- `crawl4ai-service-to-add.yml` - Contenu à ajouter au docker-compose
+- `add-to-effi.sh` - Script d'installation automatique
+- `README.md` - Cette documentation
+
+---
+
+*Intégration créée pour la stack Effi existante*
diff --git a/effi-crawl4ai/README.md b/effi-crawl4ai/README.md
new file mode 100644
index 0000000..32285c1
--- /dev/null
+++ b/effi-crawl4ai/README.md
@@ -0,0 +1,111 @@
+# Effi-Crawl4AI
+
+Service **Crawl4AI** pour le web scraping avancé dans OpenWebUI.
+
+## Architecture
+
+```
+┌─────────────────┐ HTTP API ┌─────────────────┐
+│ OpenWebUI │ ◄────────────────► │ Crawl4AI │
+│ (effi-server) │ Port 11235 │ (effi-server) │
+└─────────────────┘ └─────────────────┘
+ │ │
+ └────────────────────────────────────┘
+ Même machine (localhost)
+```
+
+**Note** : Le service tourne sur effi-server (Ubuntu) sur CPU. Version `basic` (plus légère que `all`).
+
+## Déploiement sur effi-server
+
+1. **Copier les fichiers** sur effi-server :
+```bash
+scp -r /path/to/effi-crawl4ai user@192.168.1.27:/home/user/
+```
+
+2. **Démarrer le service** :
+```bash
+cd /home/user/effi-crawl4ai
+docker-compose up -d
+```
+
+## Vérification
+
+```bash
+curl http://localhost:11235/health
+```
+
+## Configuration OpenWebUI
+
+Dans OpenWebUI (Admin → Tools → Web Search and Crawl) :
+
+**Valves à configurer :**
+- **Crawl4AI Base URL** : `http://localhost:11235` (ou `http://127.0.0.1:11235`)
+- **API Token** : (laisser vide si pas de token configuré)
+- **Timeout** : `30000` (30 secondes)
+```
+┌─────────────────┐ HTTP API ┌─────────────────┐
+│ OpenWebUI │ ◄────────────────► │ Crawl4AI │
+│ (effi-server) │ Port 11235 │ (folzo-Corp) │
+└─────────────────┘ └─────────────────┘
+```
+
+## Démarrage
+
+```bash
+docker-compose up -d
+```
+
+## Vérification
+
+```bash
+curl http://192.168.1.11:11235/health
+```
+
+## Configuration OpenWebUI
+
+Dans OpenWebUI (Admin → Tools → Web Search and Crawl) :
+
+**Valves à configurer :**
+- **Crawl4AI Base URL** : `http://192.168.1.11:11235`
+- **API Token** : (laisser vide si pas de token configuré)
+- **Timeout** : `30000` (30 secondes)
+
+## Utilisation
+
+Une fois configuré, l'agent peut :
+
+```
+"Effi, cherche des informations sur les dernières mises à jour de Fedora 42"
+"Résume-moi cet article : https://example.com/article"
+"Compare les prix de ce produit sur 3 sites différents"
+```
+
+## Fonctionnalités
+
+- ✅ Scraping JavaScript (pages dynamiques)
+- ✅ Extraction structurée (JSON, markdown)
+- ✅ Screenshot des pages
+- ✅ Respect des robots.txt
+- ✅ Rate limiting configurable
+
+## Troubleshooting
+
+**Erreur de connexion** : Vérifier que le conteneur tourne
+
+```bash
+docker ps | grep crawl4ai
+docker logs effi-crawl4ai
+```
+
+**Timeout** : Augmenter le timeout dans les Valves du Tool (60000ms = 60s)
+
+**Lenteur** : La version CPU est plus lente que la version GPU. Pour les pages lourdes en JavaScript, augmenter le timeout à 60-90s.
+
+## Limitations (version CPU)
+
+- ⏱️ Plus lent sur les pages JavaScript lourdes
+- 🎯 Limité à 3 tâches concurrentes (vs 5 sur GPU)
+- 💾 2GB RAM max (suffisant pour la plupart des sites)
+
+Pour du scraping intensif (100+ pages/heure), envisager de déplacer sur folzo-Corp avec l'image `unclecode/crawl4ai:all`.
diff --git a/effi-crawl4ai/add-to-effi.sh b/effi-crawl4ai/add-to-effi.sh
new file mode 100644
index 0000000..d5a8fdf
--- /dev/null
+++ b/effi-crawl4ai/add-to-effi.sh
@@ -0,0 +1,107 @@
+#!/bin/bash
+# Script d'ajout de Crawl4AI au docker-compose existant
+# À exécuter sur effi-server dans /root/effi/
+
+set -e
+
+echo "================================"
+echo "Ajout de Crawl4AI à Effi Server"
+echo "================================"
+echo ""
+
+# Vérifier qu'on est dans le bon répertoire
+if [ ! -f "docker-compose.yml" ]; then
+ echo "❌ Erreur: docker-compose.yml non trouvé"
+ echo " Ce script doit être exécuté dans /root/effi/"
+ exit 1
+fi
+
+echo "📄 Docker-compose trouvé"
+
+# Sauvegarder l'original
+cp docker-compose.yml docker-compose.yml.backup.$(date +%Y%m%d_%H%M%S)
+echo "💾 Backup créé: docker-compose.yml.backup.*"
+
+# Ajouter le service avant "networks:"
+echo ""
+echo "🔧 Ajout du service Crawl4AI..."
+
+# Créer un fichier temporaire avec le service à insérer
+cat > /tmp/crawl4ai-service.txt << 'EOF'
+ # ─────────────────────────────────────────────────────────
+ # 8. CRAWL4AI - Web Scraping pour OpenWebUI
+ # → http://localhost:11235 (accessible depuis OpenWebUI)
+ # ─────────────────────────────────────────────────────────
+ crawl4ai:
+ image: unclecode/crawl4ai:basic
+ container_name: crawl4ai
+ restart: unless-stopped
+ ports:
+ - "11235:11235"
+ environment:
+ - CRAWL4AI_API_TOKEN=${CRAWL4AI_API_TOKEN:-}
+ - MAX_CONCURRENT_TASKS=3
+ - CRAWL4AI_ENABLE_GPU=false
+ volumes:
+ - /dev/shm:/dev/shm
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ reservations:
+ memory: 512M
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
+ networks:
+ - effi-net
+
+EOF
+
+# Insérer avant la ligne "networks:"
+awk '
+/^networks:/ {
+ while ((getline line < "/tmp/crawl4ai-service.txt") > 0) {
+ print line
+ }
+ close("/tmp/crawl4ai-service.txt")
+}
+{ print }
+' docker-compose.yml > /tmp/docker-compose-new.yml
+
+# Remplacer le fichier original
+mv /tmp/docker-compose-new.yml docker-compose.yml
+
+echo "✅ Service ajouté au docker-compose.yml"
+echo ""
+
+# Démarrer le nouveau service
+echo "🚀 Démarrage de Crawl4AI..."
+docker-compose up -d crawl4ai
+
+echo ""
+echo "⏳ Attente du démarrage (30s)..."
+sleep 30
+
+# Vérification
+echo ""
+echo "🔍 Vérification..."
+if curl -s http://localhost:11235/health > /dev/null 2>&1; then
+ echo "✅ Crawl4AI est démarré !"
+ echo ""
+ echo "📋 Configuration OpenWebUI:"
+ echo " Admin → Tools → Web Search and Crawl → ⚙️"
+ echo " Crawl4AI Base URL: http://localhost:11235"
+ echo " Timeout: 30000"
+else
+ echo "⚠️ Le service ne répond pas encore"
+ echo " Vérifier les logs: docker logs crawl4ai"
+fi
+
+echo ""
+echo "================================"
+echo "Installation terminée !"
+echo "================================"
diff --git a/effi-crawl4ai/crawl4ai-service-to-add.yml b/effi-crawl4ai/crawl4ai-service-to-add.yml
new file mode 100644
index 0000000..a2b1d72
--- /dev/null
+++ b/effi-crawl4ai/crawl4ai-service-to-add.yml
@@ -0,0 +1,30 @@
+ # ─────────────────────────────────────────────────────────
+ # 8. CRAWL4AI - Web Scraping pour OpenWebUI
+ # → http://localhost:11235 (accessible depuis OpenWebUI)
+ # ─────────────────────────────────────────────────────────
+ crawl4ai:
+ image: unclecode/crawl4ai:basic
+ container_name: crawl4ai
+ restart: unless-stopped
+ ports:
+ - "11235:11235"
+ environment:
+ - CRAWL4AI_API_TOKEN=${CRAWL4AI_API_TOKEN:-}
+ - MAX_CONCURRENT_TASKS=3
+ - CRAWL4AI_ENABLE_GPU=false
+ volumes:
+ - /dev/shm:/dev/shm
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ reservations:
+ memory: 512M
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
+ networks:
+ - effi-net
diff --git a/effi-crawl4ai/docker-compose.crawl4ai.yml b/effi-crawl4ai/docker-compose.crawl4ai.yml
new file mode 100644
index 0000000..228ec92
--- /dev/null
+++ b/effi-crawl4ai/docker-compose.crawl4ai.yml
@@ -0,0 +1,34 @@
+# Service Crawl4AI pour Effi
+# À déployer sur folzo-Corp (GPU RX 7800 XT)
+# Exposition du port 11235 pour l'API
+
+services:
+ crawl4ai:
+ image: unclecode/crawl4ai:all
+ container_name: effi-crawl4ai
+ ports:
+ - "11235:11235"
+ environment:
+ - CRAWL4AI_API_TOKEN=${CRAWL4AI_API_TOKEN:-}
+ - MAX_CONCURRENT_TASKS=5
+ - CRAWL4AI_ENABLE_GPU=false
+ volumes:
+ - /dev/shm:/dev/shm
+ deploy:
+ resources:
+ limits:
+ memory: 4G
+ reservations:
+ memory: 1G
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
+
+networks:
+ default:
+ name: effi-network
+ external: true
diff --git a/effi-crawl4ai/docker-compose.yml b/effi-crawl4ai/docker-compose.yml
new file mode 100644
index 0000000..35171bb
--- /dev/null
+++ b/effi-crawl4ai/docker-compose.yml
@@ -0,0 +1,25 @@
+# Alternative simple à Crawl4AI pour architecture x86_64
+# Service de scraping web basique mais fonctionnel
+
+services:
+ web-scraper:
+ build:
+ context: .
+ dockerfile: Dockerfile.scraper
+ container_name: effi-web-scraper
+ ports:
+ - "11235:11235"
+ environment:
+ - MAX_CONCURRENT_TASKS=3
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 30s
+
+networks:
+ default:
+ name: effi-network
+ external: true
diff --git a/effi-crawl4ai/fix-crawl4ai.sh b/effi-crawl4ai/fix-crawl4ai.sh
new file mode 100644
index 0000000..41241af
--- /dev/null
+++ b/effi-crawl4ai/fix-crawl4ai.sh
@@ -0,0 +1,44 @@
+#!/bin/bash
+# Script de réparation de Crawl4AI
+# À exécuter sur effi-server dans /root/effi/
+
+echo "================================"
+echo "Réparation de Crawl4AI"
+echo "================================"
+echo ""
+
+# 1. Arrêter et supprimer le conteneur
+echo "🛑 Arrêt du conteneur..."
+docker stop crawl4ai 2>/dev/null || true
+docker rm crawl4ai 2>/dev/null || true
+
+# 2. Supprimer l'image corrompue
+echo "🗑️ Suppression de l'image..."
+docker rmi unclecode/crawl4ai:basic 2>/dev/null || true
+
+# 3. Nettoyer le cache Docker
+echo "🧹 Nettoyage du cache..."
+docker system prune -f
+
+# 4. Re-pull l'image avec plateforme explicite
+echo "📥 Téléchargement de l'image..."
+docker pull --platform linux/amd64 unclecode/crawl4ai:basic
+
+# 5. Redémarrer
+echo "🚀 Redémarrage..."
+docker-compose up -d crawl4ai
+
+echo ""
+echo "⏳ Attente du démarrage (60s)..."
+sleep 60
+
+# 6. Vérification
+echo ""
+echo "🔍 Vérification..."
+if curl -s http://localhost:11235/health > /dev/null 2>&1; then
+ echo "✅ Crawl4AI fonctionne !"
+ curl http://localhost:11235/health
+else
+ echo "❌ Toujours en erreur. Logs :"
+ docker logs crawl4ai --tail 20
+fi
diff --git a/effi-crawl4ai/install-on-effiserver-v2.sh b/effi-crawl4ai/install-on-effiserver-v2.sh
new file mode 100644
index 0000000..f888301
--- /dev/null
+++ b/effi-crawl4ai/install-on-effiserver-v2.sh
@@ -0,0 +1,190 @@
+#!/bin/bash
+# Installation du Web Scraper sur effi-server
+# À exécuter dans /root/effi/
+
+set -e
+
+echo "================================"
+echo "Installation Web Scraper (Effi)"
+echo "================================"
+echo ""
+
+# Vérifier qu'on est dans le bon répertoire
+if [ ! -f "docker-compose.yml" ]; then
+ echo "❌ Erreur: docker-compose.yml non trouvé"
+ exit 1
+fi
+
+echo "📁 Répertoire: $(pwd)"
+
+# Créer le répertoire pour le web scraper
+mkdir -p web-scraper
+cd web-scraper
+
+echo "📝 Création des fichiers..."
+
+# Dockerfile
+cat > Dockerfile << 'EOF'
+FROM python:3.11-slim
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4
+
+COPY scraper.py .
+
+EXPOSE 11235
+
+HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
+ CMD curl -f http://localhost:11235/health || exit 1
+
+CMD ["uvicorn", "scraper:app", "--host", "0.0.0.0", "--port", "11235"]
+EOF
+
+# Scraper Python
+cat > scraper.py << 'EOF'
+from fastapi import FastAPI, HTTPException
+from pydantic import BaseModel
+from typing import Optional, List
+import requests
+from bs4 import BeautifulSoup
+
+app = FastAPI(title="Effi Web Scraper", version="1.0.0")
+
+class ScrapeRequest(BaseModel):
+ url: str
+ max_length: Optional[int] = 10000
+
+class ScrapeResponse(BaseModel):
+ url: str
+ title: str
+ content: str
+ links: List[str]
+ success: bool
+ error: Optional[str] = None
+
+@app.get("/health")
+def health():
+ return {"status": "healthy"}
+
+@app.post("/scrape", response_model=ScrapeResponse)
+def scrape(request: ScrapeRequest):
+ try:
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+
+ response = requests.get(request.url, headers=headers, timeout=30)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.content, 'html.parser')
+
+ title = soup.title.string if soup.title else "No title"
+
+ for script in soup(["script", "style"]):
+ script.decompose()
+
+ main_content = None
+ for selector in ['main', 'article', '[role="main"]', '.content', '#content', '.post-content']:
+ main_content = soup.select_one(selector)
+ if main_content:
+ break
+
+ if not main_content:
+ main_content = soup.body if soup.body else soup
+
+ text = main_content.get_text(separator='\n', strip=True)
+ lines = [line.strip() for line in text.split('\n') if line.strip()]
+ text = '\n'.join(lines)
+
+ if len(text) > request.max_length:
+ text = text[:request.max_length] + f"\n\n[...] {len(text) - request.max_length} caractères supplémentaires"
+
+ links = []
+ for a in soup.find_all('a', href=True):
+ href = a['href']
+ if href.startswith('http'):
+ links.append(href)
+ links = links[:20]
+
+ return ScrapeResponse(
+ url=request.url,
+ title=title,
+ content=text,
+ links=links,
+ success=True
+ )
+
+ except Exception as e:
+ return ScrapeResponse(
+ url=request.url,
+ title="Error",
+ content="",
+ links=[],
+ success=False,
+ error=str(e)
+ )
+
+@app.post("/scrape/sync")
+def scrape_sync(url: str, max_length: int = 10000):
+ request = ScrapeRequest(url=url, max_length=max_length)
+ result = scrape(request)
+ if not result.success:
+ raise HTTPException(status_code=400, detail=result.error)
+ return {
+ "url": result.url,
+ "html": result.content,
+ "markdown": result.content,
+ "success": True
+ }
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=11235)
+EOF
+
+echo "✅ Fichiers créés"
+echo ""
+
+# Construire et démarrer
+echo "🔨 Construction de l'image..."
+docker build -t effi-web-scraper .
+
+echo ""
+echo "🚀 Démarrage du service..."
+docker run -d \
+ --name effi-web-scraper \
+ --restart unless-stopped \
+ -p 11235:11235 \
+ --network effi-net \
+ effi-web-scraper
+
+echo ""
+echo "⏳ Attente du démarrage (15s)..."
+sleep 15
+
+# Vérification
+echo ""
+echo "🔍 Vérification..."
+if curl -s http://localhost:11235/health > /dev/null 2>&1; then
+ echo "✅ Web Scraper est démarré !"
+ curl http://localhost:11235/health
+else
+ echo "⚠️ Le service ne répond pas encore"
+ docker logs effi-web-scraper
+fi
+
+echo ""
+echo "================================"
+echo "Installation terminée !"
+echo "================================"
+echo ""
+echo "📋 Configuration OpenWebUI:"
+echo " Admin → Tools → Web Search and Crawl → ⚙️"
+echo " Crawl4AI Base URL: http://localhost:11235"
+echo " Timeout: 30000"
+echo ""
diff --git a/effi-crawl4ai/install-on-effiserver.sh b/effi-crawl4ai/install-on-effiserver.sh
new file mode 100644
index 0000000..3a73e50
--- /dev/null
+++ b/effi-crawl4ai/install-on-effiserver.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+# Script d'installation de Crawl4AI sur effi-server
+# À copier et exécuter sur effi-server (192.168.1.27)
+
+set -e
+
+echo "================================"
+echo "Installation Crawl4AI (effi-server)"
+echo "================================"
+echo ""
+
+# 1. Créer le répertoire
+INSTALL_DIR="$HOME/effi-crawl4ai"
+mkdir -p "$INSTALL_DIR"
+cd "$INSTALL_DIR"
+
+echo "📁 Répertoire créé : $INSTALL_DIR"
+
+# 2. Créer le docker-compose.yml
+cat > docker-compose.yml << 'EOF'
+# Service Crawl4AI pour Effi (effi-server)
+# Version CPU - plus légère
+
+services:
+ crawl4ai:
+ image: unclecode/crawl4ai:basic
+ container_name: effi-crawl4ai
+ ports:
+ - "11235:11235"
+ environment:
+ - CRAWL4AI_API_TOKEN=${CRAWL4AI_API_TOKEN:-}
+ - MAX_CONCURRENT_TASKS=3
+ - CRAWL4AI_ENABLE_GPU=false
+ volumes:
+ - /dev/shm:/dev/shm
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ reservations:
+ memory: 512M
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:11235/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 60s
+EOF
+
+echo "📝 docker-compose.yml créé"
+
+# 3. Démarrer le service
+echo ""
+echo "🚀 Démarrage de Crawl4AI..."
+docker-compose up -d
+
+# 4. Attendre le démarrage
+echo ""
+echo "⏳ Attente du démarrage (30s)..."
+sleep 30
+
+# 5. Vérification
+echo ""
+echo "🔍 Vérification..."
+if curl -s http://localhost:11235/health > /dev/null 2>&1; then
+ echo "✅ Crawl4AI est démarré et accessible sur http://localhost:11235"
+else
+ echo "⚠️ Le service ne répond pas encore. Vérifier les logs :"
+ echo " docker logs effi-crawl4ai"
+fi
+
+echo ""
+echo "================================"
+echo "Installation terminée !"
+echo "================================"
+echo ""
+echo "📋 Prochaines étapes :"
+echo " 1. Dans OpenWebUI → Admin → Tools → Web Search and Crawl"
+echo " 2. Configurer la valve 'Crawl4AI Base URL' : http://localhost:11235"
+echo " 3. Activer le Tool"
+echo ""
diff --git a/effi-crawl4ai/remove-from-effi.sh b/effi-crawl4ai/remove-from-effi.sh
new file mode 100644
index 0000000..f76abef
--- /dev/null
+++ b/effi-crawl4ai/remove-from-effi.sh
@@ -0,0 +1,58 @@
+#!/bin/bash
+# Script de suppression de Crawl4AI local sur effi-server
+# À exécuter sur effi-server dans /root/effi/
+
+echo "================================"
+echo "Suppression de Crawl4AI local"
+echo "================================"
+echo ""
+
+# Vérifier qu'on est dans le bon répertoire
+if [ ! -f "docker-compose.yml" ]; then
+ echo "❌ Erreur: docker-compose.yml non trouvé"
+ echo " Ce script doit être exécuté dans /root/effi/"
+ exit 1
+fi
+
+echo "🛑 Arrêt du conteneur crawl4ai..."
+docker-compose stop crawl4ai 2>/dev/null || true
+docker-compose rm -f crawl4ai 2>/dev/null || true
+
+echo "🗑️ Suppression de l'image..."
+docker rmi unclecode/crawl4ai:basic 2>/dev/null || true
+
+echo ""
+echo "🔧 Suppression du service du docker-compose.yml..."
+
+# Créer une version sans le service crawl4ai
+awk '
+/^ # ─────────────────────────────────────────────────────────/ && printed_service == 0 {
+ getline
+ if ($0 ~ /8\. CRAWL4AI/) {
+ # On est sur le service crawl4ai, on skip jusqu'au prochain service
+ while (getline && !(/^ # ─────────────────────────────────────────────────────────/ || /^networks:/)) {}
+ # On a atteint la fin du service, on continue normalement
+ printed_service = 1
+ if (/^networks:/) {
+ print
+ next
+ }
+ }
+ # Ce n'est pas crawl4ai, on affiche
+ print prev
+ print
+ next
+}
+{ print }
+' docker-compose.yml > /tmp/docker-compose-clean.yml
+
+# Remplacer
+mv /tmp/docker-compose-clean.yml docker-compose.yml
+
+echo "✅ Service crawl4ai supprimé du docker-compose.yml"
+echo ""
+echo "📋 Prochaines étapes :"
+echo " 1. Crawl4AI tournera sur folzo-Corp (192.168.1.11:11235)"
+echo " 2. Dans OpenWebUI, configurer le Base URL :"
+echo " http://192.168.1.11:11235"
+echo ""
diff --git a/effi-crawl4ai/scraper.py b/effi-crawl4ai/scraper.py
new file mode 100644
index 0000000..f482a05
--- /dev/null
+++ b/effi-crawl4ai/scraper.py
@@ -0,0 +1,115 @@
+"""
+Service de scraping web simple pour OpenWebUI
+Alternative à Crawl4AI compatible x86_64
+"""
+
+from fastapi import FastAPI, HTTPException
+from pydantic import BaseModel
+from typing import Optional, List
+import requests
+from bs4 import BeautifulSoup
+import re
+
+app = FastAPI(title="Effi Web Scraper", version="1.0.0")
+
+class ScrapeRequest(BaseModel):
+ url: str
+ max_length: Optional[int] = 10000
+ extract_markdown: Optional[bool] = True
+
+class ScrapeResponse(BaseModel):
+ url: str
+ title: str
+ content: str
+ links: List[str]
+ success: bool
+ error: Optional[str] = None
+
+@app.get("/health")
+def health():
+ return {"status": "healthy"}
+
+@app.post("/scrape", response_model=ScrapeResponse)
+def scrape(request: ScrapeRequest):
+ try:
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
+ }
+
+ response = requests.get(request.url, headers=headers, timeout=30)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.content, 'html.parser')
+
+ # Titre
+ title = soup.title.string if soup.title else "No title"
+
+ # Supprimer scripts et styles
+ for script in soup(["script", "style"]):
+ script.decompose()
+
+ # Extraction du texte principal
+ # Essayer de trouver le contenu principal
+ main_content = None
+ for selector in ['main', 'article', '[role="main"]', '.content', '#content', '.post-content']:
+ main_content = soup.select_one(selector)
+ if main_content:
+ break
+
+ if not main_content:
+ main_content = soup.body if soup.body else soup
+
+ # Extraire le texte
+ text = main_content.get_text(separator='\n', strip=True)
+
+ # Nettoyer
+ lines = [line.strip() for line in text.split('\n') if line.strip()]
+ text = '\n'.join(lines)
+
+ # Limiter la taille
+ if len(text) > request.max_length:
+ text = text[:request.max_length] + f"\n\n[...] {len(text) - request.max_length} caractères supplémentaires"
+
+ # Extraire les liens
+ links = []
+ for a in soup.find_all('a', href=True):
+ href = a['href']
+ if href.startswith('http'):
+ links.append(href)
+ links = links[:20] # Limiter à 20 liens
+
+ return ScrapeResponse(
+ url=request.url,
+ title=title,
+ content=text,
+ links=links,
+ success=True
+ )
+
+ except Exception as e:
+ return ScrapeResponse(
+ url=request.url,
+ title="Error",
+ content="",
+ links=[],
+ success=False,
+ error=str(e)
+ )
+
+@app.post("/scrape/sync")
+def scrape_sync(url: str, max_length: int = 10000):
+ """Endpoint compatible avec l'API Crawl4AI"""
+ request = ScrapeRequest(url=url, max_length=max_length)
+ result = scrape(request)
+ if not result.success:
+ raise HTTPException(status_code=400, detail=result.error)
+ return {
+ "url": result.url,
+ "html": result.content,
+ "markdown": result.content,
+ "success": True
+ }
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=11235)
diff --git a/effi-crawl4ai/update-effiserver.sh b/effi-crawl4ai/update-effiserver.sh
new file mode 100644
index 0000000..d2bf03b
--- /dev/null
+++ b/effi-crawl4ai/update-effiserver.sh
@@ -0,0 +1,215 @@
+#!/bin/bash
+# Script de mise à jour du Web Scraper sur effi-server
+# À exécuter dans /root/effi/
+
+set -e
+
+echo "================================"
+echo "Mise à jour Web Scraper"
+echo "================================"
+echo ""
+
+# 1. Nettoyer l'ancien conteneur standalone
+echo "🧹 Nettoyage de l'ancien conteneur..."
+docker stop effi-web-scraper 2>/dev/null || true
+docker rm effi-web-scraper 2>/dev/null || true
+
+# 2. Nettoyer le réseau si créé
+echo "🗑️ Suppression du réseau effi-net temporaire..."
+docker network rm effi-net 2>/dev/null || true
+
+# 3. Supprimer l'ancienne section crawl4ai du docker-compose
+echo "📝 Mise à jour du docker-compose.yml..."
+
+# Créer le service web-scraper à insérer
+cat > /tmp/web-scraper-service.txt << 'EOF'
+ # ─────────────────────────────────────────────────────────
+ # 8. WEB SCRAPER - Scraping pour OpenWebUI
+ # → http://localhost:11235
+ # ─────────────────────────────────────────────────────────
+ web-scraper:
+ build: ./web-scraper
+ container_name: effi-web-scraper
+ restart: unless-stopped
+ ports:
+ - "11235:11235"
+ networks:
+ - effi-net
+
+EOF
+
+# Backup du docker-compose
+cp docker-compose.yml docker-compose.yml.backup.$(date +%Y%m%d_%H%M%S)
+
+# Utiliser awk pour remplacer la section 8 (crawl4ai) par web-scraper
+awk '
+/^networks:/ {
+ # On est à la fin du fichier services, inserer le nouveau service avant
+ system("cat /tmp/web-scraper-service.txt")
+}
+{ print }
+' docker-compose.yml > /tmp/docker-compose-new.yml
+
+mv /tmp/docker-compose-new.yml docker-compose.yml
+
+echo "✅ Section #8 mise à jour (crawl4ai → web-scraper)"
+
+# 4. Créer le répertoire et les fichiers du web-scraper
+echo ""
+echo "📁 Création du répertoire web-scraper..."
+mkdir -p web-scraper
+
+cat > web-scraper/Dockerfile << 'EOF'
+FROM python:3.11-slim
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ curl \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN pip install --no-cache-dir fastapi uvicorn requests beautifulsoup4
+
+COPY scraper.py .
+
+EXPOSE 11235
+
+HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
+ CMD curl -f http://localhost:11235/health || exit 1
+
+CMD ["uvicorn", "scraper:app", "--host", "0.0.0.0", "--port", "11235"]
+EOF
+
+cat > web-scraper/scraper.py << 'EOF'
+from fastapi import FastAPI, HTTPException
+from pydantic import BaseModel
+from typing import Optional, List
+import requests
+from bs4 import BeautifulSoup
+
+app = FastAPI(title="Effi Web Scraper", version="1.0.0")
+
+class ScrapeRequest(BaseModel):
+ url: str
+ max_length: Optional[int] = 10000
+
+class ScrapeResponse(BaseModel):
+ url: str
+ title: str
+ content: str
+ links: List[str]
+ success: bool
+ error: Optional[str] = None
+
+@app.get("/health")
+def health():
+ return {"status": "healthy"}
+
+@app.post("/scrape", response_model=ScrapeResponse)
+def scrape(request: ScrapeRequest):
+ try:
+ headers = {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
+ }
+
+ response = requests.get(request.url, headers=headers, timeout=30)
+ response.raise_for_status()
+
+ soup = BeautifulSoup(response.content, 'html.parser')
+
+ title = soup.title.string if soup.title else "No title"
+
+ for script in soup(["script", "style"]):
+ script.decompose()
+
+ main_content = None
+ for selector in ['main', 'article', '[role="main"]', '.content', '#content', '.post-content']:
+ main_content = soup.select_one(selector)
+ if main_content:
+ break
+
+ if not main_content:
+ main_content = soup.body if soup.body else soup
+
+ text = main_content.get_text(separator='\n', strip=True)
+ lines = [line.strip() for line in text.split('\n') if line.strip()]
+ text = '\n'.join(lines)
+
+ if len(text) > request.max_length:
+ text = text[:request.max_length] + f"\n\n[...] {len(text) - request.max_length} caractères supplémentaires"
+
+ links = []
+ for a in soup.find_all('a', href=True):
+ href = a['href']
+ if href.startswith('http'):
+ links.append(href)
+ links = links[:20]
+
+ return ScrapeResponse(
+ url=request.url,
+ title=title,
+ content=text,
+ links=links,
+ success=True
+ )
+
+ except Exception as e:
+ return ScrapeResponse(
+ url=request.url,
+ title="Error",
+ content="",
+ links=[],
+ success=False,
+ error=str(e)
+ )
+
+@app.post("/scrape/sync")
+def scrape_sync(url: str, max_length: int = 10000):
+ request = ScrapeRequest(url=url, max_length=max_length)
+ result = scrape(request)
+ if not result.success:
+ raise HTTPException(status_code=400, detail=result.error)
+ return {
+ "url": result.url,
+ "html": result.content,
+ "markdown": result.content,
+ "success": True
+ }
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=11235)
+EOF
+
+echo "✅ Fichiers créés dans web-scraper/"
+
+# 5. Relancer docker-compose
+echo ""
+echo "🚀 Démarrage du service web-scraper..."
+docker-compose up -d web-scraper
+
+echo ""
+echo "⏳ Attente du démarrage (30s)..."
+sleep 30
+
+# 6. Vérification
+echo ""
+echo "🔍 Vérification..."
+if curl -s http://localhost:11235/health > /dev/null 2>&1; then
+ echo "✅ Web Scraper est fonctionnel !"
+ curl http://localhost:11235/health
+else
+ echo "⚠️ Le service ne répond pas encore"
+ docker logs effi-web-scraper
+fi
+
+echo ""
+echo "================================"
+echo "Installation terminée !"
+echo "================================"
+echo ""
+echo "📋 Configuration OpenWebUI:"
+echo " Admin → Tools → Web Search and Crawl → ⚙️"
+echo " Crawl4AI Base URL: http://localhost:11235"
+echo " Timeout: 30000"
+echo " ✅ Activer le Tool"
diff --git a/effi-ocr/Dockerfile b/effi-ocr/Dockerfile
new file mode 100644
index 0000000..6085dc3
--- /dev/null
+++ b/effi-ocr/Dockerfile
@@ -0,0 +1,20 @@
+FROM python:3.11-slim
+# Installer Tesseract et les langues
+RUN apt-get update && apt-get install -y \
+ tesseract-ocr \
+ tesseract-ocr-fra \
+ tesseract-ocr-eng \
+ libtesseract-dev \
+ && rm -rf /var/lib/apt/lists/*
+# Créer le répertoire de travail
+WORKDIR /app
+# Copier les dépendances
+COPY requirements.txt .
+# Installer les dépendances Python
+RUN pip install --no-cache-dir -r requirements.txt
+# Copier le code
+COPY main.py .
+# Exposer le port
+EXPOSE 8300
+# Lancer l'application
+CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8300"]
diff --git a/effi-ocr/README.md b/effi-ocr/README.md
new file mode 100644
index 0000000..aafc8a4
--- /dev/null
+++ b/effi-ocr/README.md
@@ -0,0 +1,232 @@
+# 📝 Effi-OCR
+
+Service OCR (Optical Character Recognition) pour Effi. Extrait le texte des images et photos de documents.
+
+---
+
+## 🎯 Fonctionnalités
+
+- Extraction de texte à partir d'images (JPEG, PNG, GIF, BMP, TIFF, WebP)
+- Support multilingue (Français, Anglais, Allemand, Espagnol, Italien)
+- Prétraitement automatique pour améliorer la qualité
+- API REST simple et rapide
+- Intégration OpenWebUI via Tool
+
+---
+
+## 🚀 Démarrage rapide
+
+```bash
+# Construire et démarrer
+cd ~/Documents/OpenCode/Projets/Effi/effi-ocr
+sudo docker compose up -d --build
+
+# Vérifier le service
+curl http://localhost:8300/health
+```
+
+---
+
+## 📡 API Endpoints
+
+### Health Check
+```bash
+GET /health
+```
+
+### OCR - Upload fichier
+```bash
+POST /ocr
+Content-Type: multipart/form-data
+
+Parameters:
+ - file: Image à analyser
+ - lang: Code langue (fra, eng, fra+eng, etc.)
+ - preprocess: true/false (prétraitement auto)
+```
+
+Exemple:
+```bash
+curl -X POST -F "file=@photo.jpg" -F "lang=fra" http://localhost:8300/ocr
+```
+
+### OCR - Base64
+```bash
+POST /ocr/base64
+Content-Type: application/json
+
+{
+ "image_base64": "...",
+ "lang": "fra",
+ "preprocess": true
+}
+```
+
+### Langues disponibles
+```bash
+GET /languages
+```
+
+---
+
+## 🔧 Configuration
+
+### Variables d'environnement
+
+| Variable | Défaut | Description |
+|----------|--------|-------------|
+| `OCR_URL` | `http://192.168.1.11:8300` | URL du service OCR |
+
+### Langues supportées
+
+- `fra` - Français
+- `eng` - Anglais
+- `fra+eng` - Bilingue FR+EN
+- `deu` - Allemand
+- `spa` - Espagnol
+- `ita` - Italien
+
+---
+
+## 🐳 Docker
+
+### Construction
+```bash
+sudo docker compose build
+```
+
+### Démarrage
+```bash
+sudo docker compose up -d
+```
+
+### Logs
+```bash
+sudo docker logs effi-ocr -f
+```
+
+### Arrêt
+```bash
+sudo docker compose down
+```
+
+---
+
+## 🔗 Intégration OpenWebUI
+
+Voir [TOOL-OPENWEBUI.md](TOOL-OPENWEBUI.md) pour la configuration complète du Tool.
+
+**Résumé rapide:**
+1. Admin → Tools → Create Tool
+2. Copier le code Python fourni
+3. Configurer la variable `OCR_URL`
+4. Activer le Tool dans les paramètres de conversation
+
+---
+
+## 🧪 Tests
+
+### Test de santé
+```bash
+curl http://localhost:8300/health
+```
+
+### Test OCR simple
+```bash
+curl -X POST \
+ -F "file=@/chemin/vers/image.jpg" \
+ -F "lang=fra" \
+ http://localhost:8300/ocr
+```
+
+### Test avec prétraitement désactivé
+```bash
+curl -X POST \
+ -F "file=@image.png" \
+ -F "lang=eng" \
+ -F "preprocess=false" \
+ http://localhost:8300/ocr
+```
+
+---
+
+## 📊 Performance
+
+- **Temps de traitement** : ~1-3 secondes par image (dépend de la taille)
+- **Formats supportés** : JPEG, PNG, GIF, BMP, TIFF, WebP
+- **Taille max recommandée** : 2048x2048 pixels
+- **Prétraitement** : Conversion niveaux de gris + contraste (optionnel)
+
+---
+
+## 🐛 Dépannage
+
+### Service ne répond pas
+```bash
+# Vérifier si le conteneur tourne
+sudo docker ps | grep effi-ocr
+
+# Redémarrer
+sudo docker restart effi-ocr
+
+# Voir les logs
+sudo docker logs effi-ocr
+```
+
+### Tesseract non trouvé
+L'erreur indique que Tesseract n'est pas installé dans le conteneur. Reconstruire:
+```bash
+sudo docker compose down
+sudo docker compose up -d --build
+```
+
+### OCR de mauvaise qualité
+- Utiliser `lang=fra+eng` pour les documents mixtes
+- S'assurer que l'image est bien éclairée
+- Vérifier la résolution (300 DPI recommandé pour documents)
+- Activer le prétraitement (par défaut: true)
+
+---
+
+## 📁 Structure
+
+```
+effi-ocr/
+├── main.py # API FastAPI
+├── Dockerfile # Configuration Docker
+├── docker-compose.yml # Orchestration
+├── requirements.txt # Dépendances Python
+├── README.md # Ce fichier
+└── TOOL-OPENWEBUI.md # Guide Tool OpenWebUI
+```
+
+---
+
+## 📝 Dépendances
+
+- Python 3.11
+- FastAPI
+- Pillow (traitement d'images)
+- pytesseract (wrapper Tesseract)
+- Tesseract OCR 5.x
+
+---
+
+## 🎨 Cas d'usage
+
+- **Numérisation de livres** : Extraire le texte de photos de pages
+- **Factures** : Récupérer les informations de factures scannées
+- **Documents** : OCR sur PDF et images de documents
+- **Notes manuscrites** : Reconnaissance de texte manuscrit (limitée)
+
+---
+
+## 🔒 Sécurité
+
+- Le service écoute uniquement sur localhost (127.0.0.1:8300)
+- Aucune donnée n'est stockée persistante
+- Les images sont traitées en mémoire et supprimées après
+
+---
+
+Créé le 6 Avril 2026
diff --git a/effi-ocr/TOOL-OPENWEBUI.md b/effi-ocr/TOOL-OPENWEBUI.md
new file mode 100644
index 0000000..e379894
--- /dev/null
+++ b/effi-ocr/TOOL-OPENWEBUI.md
@@ -0,0 +1,175 @@
+# Tool OCR pour OpenWebUI
+
+## Configuration
+
+Dans OpenWebUI (http://192.168.1.27:3000) :
+
+1. **Aller dans** : Admin → Tools → Create Tool
+2. **Nom** : OCR Extracteur de Texte
+3. **ID** : ocr_extractor
+4. **Description** : Extrait le texte des images pour les livres et documents
+
+## Code du Tool
+
+```python
+from typing import Optional
+import requests
+import base64
+import os
+
+class Tools:
+ def __init__(self):
+ """Initialize OCR Tool"""
+ self.ocr_url = os.getenv("OCR_URL", "http://192.168.1.11:8300")
+
+ def extract_text_from_image(self, image_path: str, language: str = "fra") -> str:
+ """
+ Extract text from an image file using OCR
+
+ Args:
+ image_path: Path to the image file (jpg, png, etc.)
+ language: Language code (fra, eng, fra+eng, etc.)
+
+ Returns:
+ Extracted text from the image
+ """
+ try:
+ # Read image file
+ with open(image_path, 'rb') as f:
+ files = {'file': f}
+ data = {'lang': language}
+
+ response = requests.post(
+ f"{self.ocr_url}/ocr",
+ files=files,
+ data=data,
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('success'):
+ text = result.get('text', '')
+ char_count = result.get('characters_count', 0)
+ word_count = result.get('words_count', 0)
+ return f"Texte extrait ({char_count} caractères, {word_count} mots):\n\n{text}"
+ else:
+ return f"Erreur OCR: {result.get('error', 'Unknown error')}"
+ else:
+ return f"Erreur HTTP {response.status_code}: {response.text}"
+
+ except FileNotFoundError:
+ return f"Erreur: Fichier non trouvé: {image_path}"
+ except Exception as e:
+ return f"Erreur lors de l'OCR: {str(e)}"
+
+ def extract_text_from_base64(self, image_base64: str, language: str = "fra") -> str:
+ """
+ Extract text from a base64 encoded image
+
+ Args:
+ image_base64: Base64 encoded image string
+ language: Language code (fra, eng, fra+eng, etc.)
+
+ Returns:
+ Extracted text from the image
+ """
+ try:
+ response = requests.post(
+ f"{self.ocr_url}/ocr/base64",
+ json={
+ 'image_base64': image_base64,
+ 'lang': language,
+ 'preprocess': True
+ },
+ timeout=30
+ )
+
+ if response.status_code == 200:
+ result = response.json()
+ if result.get('success'):
+ text = result.get('text', '')
+ char_count = result.get('characters_count', 0)
+ word_count = result.get('words_count', 0)
+ return f"Texte extrait ({char_count} caractères, {word_count} mots):\n\n{text}"
+ else:
+ return f"Erreur OCR: extraction échouée"
+ else:
+ return f"Erreur HTTP {response.status_code}: {response.text}"
+
+ except Exception as e:
+ return f"Erreur lors de l'OCR: {str(e)}"
+
+ def get_available_languages(self) -> str:
+ """
+ Get list of available OCR languages
+
+ Returns:
+ List of available language codes
+ """
+ try:
+ response = requests.get(f"{self.ocr_url}/languages", timeout=10)
+
+ if response.status_code == 200:
+ result = response.json()
+ languages = result.get('available_languages', [])
+ return f"Langues disponibles: {', '.join(languages)}"
+ else:
+ return f"Erreur: {response.status_code}"
+
+ except Exception as e:
+ return f"Erreur: {str(e)}"
+```
+
+## Variables d'environnement
+
+Dans les Settings d'OpenWebUI (Admin → Settings → Environment Variables) :
+
+```
+OCR_URL=http://192.168.1.11:8300
+```
+
+## Utilisation
+
+Une fois le Tool créé, les agents pourront l'utiliser avec ces fonctions :
+
+- `extract_text_from_image("/chemin/vers/image.jpg", "fra")` - Extraire le texte d'un fichier image
+- `extract_text_from_base64("base64encoded...", "fra")` - Extraire d'une image encodée en base64
+- `get_available_languages()` - Lister les langues disponibles
+
+## Exemple de prompt
+
+```
+"Effi, lis-moi ce texte" [upload d'image]
+→ L'agent utilise automatiquement le Tool OCR
+```
+
+## Langues disponibles
+
+- `fra` - Français
+- `eng` - Anglais
+- `fra+eng` - Français + Anglais (bilingue)
+- `deu` - Allemand
+- `spa` - Espagnol
+- `ita` - Italien
+
+## Dépannage
+
+**Erreur de connexion** :
+```bash
+# Vérifier que le service OCR tourne
+curl http://192.168.1.11:8300/health
+
+# Redémarrer si nécessaire
+cd ~/Documents/OpenCode/Projets/Effi/effi-ocr
+sudo docker compose restart
+```
+
+**Texte mal extrait** :
+- Essayer `fra+eng` pour les documents bilingues
+- Vérifier la qualité de l'image (résolution, contraste)
+- L'OCR fonctionne mieux avec des images bien éclairées
+
+---
+
+*Tool créé le 6 Avril 2026*
diff --git a/effi-ocr/docker-compose.yml b/effi-ocr/docker-compose.yml
new file mode 100644
index 0000000..432cb98
--- /dev/null
+++ b/effi-ocr/docker-compose.yml
@@ -0,0 +1,22 @@
+services:
+ effi-ocr:
+ build: .
+ container_name: effi-ocr
+ ports:
+ - "8300:8300"
+ restart: unless-stopped
+ environment:
+ - PYTHONUNBUFFERED=1
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8300/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 10s
+ networks:
+ - effi-network
+
+networks:
+ effi-network:
+ external: true
+ name: effi-network
diff --git a/effi-ocr/main.py b/effi-ocr/main.py
new file mode 100644
index 0000000..aefba3d
--- /dev/null
+++ b/effi-ocr/main.py
@@ -0,0 +1,208 @@
+"""
+Effi-OCR : Service OCR pour Effi
+API FastAPI qui expose Tesseract via une interface REST
+"""
+
+from fastapi import FastAPI, File, UploadFile, HTTPException
+from fastapi.responses import JSONResponse
+from PIL import Image
+import pytesseract
+import io
+import logging
+from typing import Optional
+
+# Configuration logging
+logging.basicConfig(level=logging.INFO)
+logger = logging.getLogger(__name__)
+
+app = FastAPI(
+ title="Effi-OCR",
+ description="Service OCR pour extraction de texte d'images",
+ version="1.0.0"
+)
+
+
+@app.get("/health")
+async def health_check():
+ """Endpoint de santé pour monitoring"""
+ try:
+ # Vérifier que Tesseract est accessible
+ import subprocess
+ result = subprocess.run(['tesseract', '--version'], capture_output=True, text=True)
+ version_line = result.stdout.split('\n')[0] if result.stdout else "unknown"
+ tesseract_version = str(version_line) # Forcer en string
+ except Exception as e:
+ tesseract_version = f"error: {str(e)}"
+
+ return {
+ "status": "healthy",
+ "service": "effi-ocr",
+ "version": "1.0.0",
+ "tesseract_version": tesseract_version
+ }
+
+
+@app.post("/ocr")
+async def ocr_image(
+ file: UploadFile = File(...),
+ lang: str = "fra",
+ preprocess: bool = True
+):
+ """
+ Extraire le texte d'une image
+
+ Args:
+ file: Image à analyser (jpg, png, etc.)
+ lang: Code langue (fra, eng, fra+eng, etc.)
+ preprocess: Appliquer un prétraitement pour améliorer la qualité
+
+ Returns:
+ JSON avec le texte extrait et les métadonnées
+ """
+ logger.info(f"OCR demandé - lang: {lang}, fichier: {file.filename}")
+
+ # Vérifier le type de fichier
+ allowed_types = ['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/tiff', 'image/webp']
+ if file.content_type not in allowed_types:
+ raise HTTPException(
+ status_code=400,
+ detail=f"Type de fichier non supporté: {file.content_type}. Types acceptés: {', '.join(allowed_types)}"
+ )
+
+ try:
+ # Lire l'image
+ contents = await file.read()
+ image = Image.open(io.BytesIO(contents))
+
+ # Convertir en RGB si nécessaire (pour les images avec transparence)
+ if image.mode in ('RGBA', 'LA', 'P'):
+ image = image.convert('RGB')
+
+ # Prétraitement optionnel pour améliorer la qualité OCR
+ if preprocess:
+ # Convertir en niveaux de gris pour meilleure détection texte
+ image = image.convert('L')
+ # Augmenter le contraste
+ from PIL import ImageEnhance
+ enhancer = ImageEnhance.Contrast(image)
+ image = enhancer.enhance(2.0)
+
+ # OCR avec Tesseract
+ text = pytesseract.image_to_string(image, lang=lang)
+
+ # Nettoyer le texte
+ text = text.strip()
+
+ # Informations sur l'image
+ width, height = image.size
+
+ logger.info(f"OCR terminé - {len(text)} caractères extraits")
+
+ return JSONResponse(content={
+ "success": True,
+ "text": text,
+ "language": lang,
+ "filename": file.filename,
+ "image_size": {"width": width, "height": height},
+ "characters_count": len(text),
+ "words_count": len(text.split()) if text else 0
+ })
+
+ except Exception as e:
+ logger.error(f"Erreur OCR: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Erreur lors de l'OCR: {str(e)}")
+
+
+@app.post("/ocr/base64")
+async def ocr_base64(
+ image_base64: str,
+ lang: str = "fra",
+ preprocess: bool = True
+):
+ """
+ Extraire le texte d'une image en base64
+
+ Args:
+ image_base64: Image encodée en base64
+ lang: Code langue (fra, eng, fra+eng, etc.)
+ preprocess: Appliquer un prétraitement
+
+ Returns:
+ JSON avec le texte extrait
+ """
+ import base64
+
+ logger.info(f"OCR base64 demandé - lang: {lang}")
+
+ try:
+ # Décoder le base64
+ image_data = base64.b64decode(image_base64)
+ image = Image.open(io.BytesIO(image_data))
+
+ # Convertir en RGB si nécessaire
+ if image.mode in ('RGBA', 'LA', 'P'):
+ image = image.convert('RGB')
+
+ # Prétraitement
+ if preprocess:
+ image = image.convert('L')
+ from PIL import ImageEnhance
+ enhancer = ImageEnhance.Contrast(image)
+ image = enhancer.enhance(2.0)
+
+ # OCR
+ text = pytesseract.image_to_string(image, lang=lang)
+ text = text.strip()
+
+ width, height = image.size
+
+ logger.info(f"OCR base64 terminé - {len(text)} caractères")
+
+ return {
+ "success": True,
+ "text": text,
+ "language": lang,
+ "image_size": {"width": width, "height": height},
+ "characters_count": len(text),
+ "words_count": len(text.split()) if text else 0
+ }
+
+ except Exception as e:
+ logger.error(f"Erreur OCR base64: {str(e)}")
+ raise HTTPException(status_code=500, detail=f"Erreur lors de l'OCR: {str(e)}")
+
+
+@app.get("/languages")
+async def list_languages():
+ """Lister les langues disponibles pour l'OCR"""
+ try:
+ # Commande tesseract pour lister les langues
+ import subprocess
+ result = subprocess.run(
+ ['tesseract', '--list-langs'],
+ capture_output=True,
+ text=True
+ )
+
+ # Parser la sortie
+ lines = result.stdout.strip().split('\n')
+ # La première ligne est "List of available languages (xxx):"
+ languages = lines[1:] if len(lines) > 1 else []
+
+ return {
+ "available_languages": languages,
+ "count": len(languages)
+ }
+ except Exception as e:
+ logger.error(f"Erreur listage langues: {str(e)}")
+ # Fallback avec les langues communes
+ return {
+ "available_languages": ["fra", "eng", "fra+eng", "deu", "spa", "ita"],
+ "count": 6,
+ "note": "Liste par défaut, erreur lors de la récupération"
+ }
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run(app, host="0.0.0.0", port=8300)
diff --git a/effi-ocr/requirements.txt b/effi-ocr/requirements.txt
new file mode 100644
index 0000000..9a579d7
--- /dev/null
+++ b/effi-ocr/requirements.txt
@@ -0,0 +1,5 @@
+fastapi==0.109.0
+uvicorn[standard]==0.27.0
+python-multipart==0.0.6
+pillow==10.2.0
+pytesseract==0.3.10
diff --git a/scripts/start-all-services.sh b/scripts/start-all-services.sh
index f00a53f..ccb00f8 100755
--- a/scripts/start-all-services.sh
+++ b/scripts/start-all-services.sh
@@ -44,6 +44,8 @@ echo ""
start_service "Ollama" "$HOME" "docker-compose.yml"
start_service "ComfyUI" "$HOME/Documents/OpenCode/Projets/Effi/comfyui"
start_service "Effi-STT" "$HOME/Documents/OpenCode/Projets/Effi/effi-stt"
+start_service "Effi-OCR" "$HOME/Documents/OpenCode/Projets/Effi/effi-ocr"
+start_service "Effi-Crawl4AI" "$HOME/Documents/OpenCode/Projets/Effi/effi-crawl4ai"
start_service "openedai-speech" "$HOME/openedai-speech"
echo ""
@@ -56,7 +58,7 @@ sleep 5
# Vérifier que tout répond
services_ok=0
-services_total=4
+services_total=6
echo -n "Ollama (11434)... "
if curl -s http://localhost:11434 > /dev/null 2>&1; then
@@ -82,6 +84,22 @@ else
echo -e "${RED}✗${NC}"
fi
+echo -n "Effi-OCR (8300)... "
+if curl -s http://localhost:8300/health > /dev/null 2>&1; then
+ echo -e "${GREEN}✓${NC}"
+ ((services_ok++))
+else
+ echo -e "${RED}✗${NC}"
+fi
+
+echo -n "Effi-Crawl4AI (11235)... "
+if curl -s http://localhost:11235/health > /dev/null 2>&1; then
+ echo -e "${GREEN}✓${NC}"
+ ((services_ok++))
+else
+ echo -e "${RED}✗${NC}"
+fi
+
echo -n "openedai-speech (8100)... "
if curl -s http://localhost:8100/health > /dev/null 2>&1; then
echo -e "${GREEN}✓${NC}"
@@ -103,10 +121,11 @@ if [ $services_ok -eq $services_total ]; then
else
echo -e "${RED}Certains services n'ont pas démarré.${NC}"
echo ""
- echo "Pour voir les logs :"
- echo " sudo docker logs ollama"
- echo " sudo docker logs comfyui"
- echo " sudo docker logs effi-stt"
- echo " sudo docker logs openedai-speech"
+echo "Pour voir les logs :"
+echo " sudo docker logs ollama"
+echo " sudo docker logs comfyui"
+echo " sudo docker logs effi-stt"
+echo " sudo docker logs effi-ocr"
+echo " sudo docker logs openedai-speech"
exit 1
fi