Ajout Effi-OCR (Tesseract) + Web Scraper pour OpenWebUI
- Effi-OCR: Service OCR avec Tesseract (multilingue FR/EN) * API FastAPI sur port 8300 * Prétraitement images (contraste, niveaux de gris) * Tool OpenWebUI intégré - Web Scraper: Service Python (BeautifulSoup) sur port 11235 * Alternative à Crawl4AI (incompatible ARM64/J5040) * Scraping HTML basique mais fonctionnel * Intégré au docker-compose d'effi-server - Documentation: * ROADMAP-FONCTIONNALITES.md: Plan des 9 fonctionnalités * WEB-SCRAPER-CONFIG.md: Configuration OpenWebUI * WEB-SCRAPER-TOOL.md: Code du Tool - Script start-all-services.sh mis à jour (6 services)
This commit is contained in:
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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'<script[^>]*>[^<]*</script>', '', html, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
text = re.sub(r'<style[^>]*>[^<]*</style>', '', 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'<title[^>]*>([^<]*)</title>', 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'<article[^>]*>(.*?)</article>',
|
||||||
|
r'<main[^>]*>(.*?)</main>',
|
||||||
|
r'<div[^>]*class=["\'][^"\']*(?:content|article|post)[^"\']*["\'][^>]*>(.*?)</div>',
|
||||||
|
]
|
||||||
|
|
||||||
|
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'<body[^>]*>(.*?)</body>', html, re.IGNORECASE | re.DOTALL)
|
||||||
|
content = body_match.group(1) if body_match else html
|
||||||
|
|
||||||
|
# Nettoyer le contenu
|
||||||
|
content = re.sub(r'<script[^>]*>[^<]*</script>', '', content, flags=re.IGNORECASE | re.DOTALL)
|
||||||
|
content = re.sub(r'<style[^>]*>[^<]*</style>', '', 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*
|
||||||
@@ -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"]
|
||||||
@@ -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"]
|
||||||
@@ -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*
|
||||||
@@ -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*
|
||||||
@@ -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`.
|
||||||
@@ -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 "================================"
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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 ""
|
||||||
@@ -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 ""
|
||||||
@@ -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 ""
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
@@ -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"]
|
||||||
@@ -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
|
||||||
@@ -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*
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -44,6 +44,8 @@ echo ""
|
|||||||
start_service "Ollama" "$HOME" "docker-compose.yml"
|
start_service "Ollama" "$HOME" "docker-compose.yml"
|
||||||
start_service "ComfyUI" "$HOME/Documents/OpenCode/Projets/Effi/comfyui"
|
start_service "ComfyUI" "$HOME/Documents/OpenCode/Projets/Effi/comfyui"
|
||||||
start_service "Effi-STT" "$HOME/Documents/OpenCode/Projets/Effi/effi-stt"
|
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"
|
start_service "openedai-speech" "$HOME/openedai-speech"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
@@ -56,7 +58,7 @@ sleep 5
|
|||||||
|
|
||||||
# Vérifier que tout répond
|
# Vérifier que tout répond
|
||||||
services_ok=0
|
services_ok=0
|
||||||
services_total=4
|
services_total=6
|
||||||
|
|
||||||
echo -n "Ollama (11434)... "
|
echo -n "Ollama (11434)... "
|
||||||
if curl -s http://localhost:11434 > /dev/null 2>&1; then
|
if curl -s http://localhost:11434 > /dev/null 2>&1; then
|
||||||
@@ -82,6 +84,22 @@ else
|
|||||||
echo -e "${RED}✗${NC}"
|
echo -e "${RED}✗${NC}"
|
||||||
fi
|
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)... "
|
echo -n "openedai-speech (8100)... "
|
||||||
if curl -s http://localhost:8100/health > /dev/null 2>&1; then
|
if curl -s http://localhost:8100/health > /dev/null 2>&1; then
|
||||||
echo -e "${GREEN}✓${NC}"
|
echo -e "${GREEN}✓${NC}"
|
||||||
@@ -107,6 +125,7 @@ else
|
|||||||
echo " sudo docker logs ollama"
|
echo " sudo docker logs ollama"
|
||||||
echo " sudo docker logs comfyui"
|
echo " sudo docker logs comfyui"
|
||||||
echo " sudo docker logs effi-stt"
|
echo " sudo docker logs effi-stt"
|
||||||
|
echo " sudo docker logs effi-ocr"
|
||||||
echo " sudo docker logs openedai-speech"
|
echo " sudo docker logs openedai-speech"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user