mirror of
https://github.com/Ladebeze66/ragflow_preprocess.git
synced 2026-02-04 07:10:25 +01:00
first commit
This commit is contained in:
commit
3ac1fa1617
174
README.md
Normal file
174
README.md
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
# Prétraitement PDF pour Ragflow
|
||||||
|
|
||||||
|
Outil de prétraitement de documents PDF avec agents LLM modulables pour l'analyse, la traduction et le résumé.
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
- **Sélection visuelle** de zones dans les documents PDF (tableaux, schémas, formules, texte)
|
||||||
|
- **Analyse automatique** avec différents agents LLM configurables
|
||||||
|
- **Niveaux d'analyse** adaptables selon les besoins (léger, moyen, avancé)
|
||||||
|
- **Export Markdown** pour intégration dans une base Ragflow
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Prérequis
|
||||||
|
|
||||||
|
- Python 3.8 ou supérieur
|
||||||
|
- PyQt6
|
||||||
|
- PyMuPDF (fitz)
|
||||||
|
- Tesseract OCR (pour la reconnaissance de texte)
|
||||||
|
- Ollama (pour les modèles LLM)
|
||||||
|
|
||||||
|
### Méthode 1 : Installation directe (recommandée)
|
||||||
|
|
||||||
|
Clonez ce dépôt et installez avec pip :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/votre-utilisateur/ragflow-pretraitement.git
|
||||||
|
cd ragflow_pretraitement
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
Cette méthode installera automatiquement toutes les dépendances Python requises.
|
||||||
|
|
||||||
|
### Méthode 2 : Installation manuelle
|
||||||
|
|
||||||
|
Si vous préférez une installation manuelle :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installation de Tesseract OCR
|
||||||
|
|
||||||
|
Pour l'OCR, vous devez également installer Tesseract :
|
||||||
|
|
||||||
|
- **Windows** : Téléchargez et installez depuis [https://github.com/UB-Mannheim/tesseract/wiki](https://github.com/UB-Mannheim/tesseract/wiki)
|
||||||
|
- **Linux** : `sudo apt install tesseract-ocr tesseract-ocr-fra tesseract-ocr-eng`
|
||||||
|
- **macOS** : `brew install tesseract`
|
||||||
|
|
||||||
|
### Installation d'Ollama
|
||||||
|
|
||||||
|
Suivez les instructions d'installation d'Ollama disponibles sur [https://ollama.ai/](https://ollama.ai/)
|
||||||
|
|
||||||
|
Modèles recommandés à télécharger :
|
||||||
|
```bash
|
||||||
|
ollama pull llava:34b
|
||||||
|
ollama pull mistral
|
||||||
|
ollama pull llama3.2-vision
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
### Lancement de l'application
|
||||||
|
|
||||||
|
Si vous avez utilisé l'installation avec le script setup.py :
|
||||||
|
```bash
|
||||||
|
ragflow-pretraitement
|
||||||
|
```
|
||||||
|
|
||||||
|
Ou manuellement :
|
||||||
|
```bash
|
||||||
|
cd ragflow_pretraitement
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Processus typique
|
||||||
|
|
||||||
|
1. Charger un PDF avec le bouton "Charger PDF"
|
||||||
|
2. Naviguer entre les pages avec les boutons ou le sélecteur
|
||||||
|
3. Sélectionner une zone d'intérêt avec la souris
|
||||||
|
4. Choisir le type de contenu (schéma, tableau, formule...)
|
||||||
|
5. Ajouter un contexte textuel si nécessaire
|
||||||
|
6. Configurer les agents LLM dans l'onglet "Agents LLM"
|
||||||
|
7. Appliquer l'agent sur la sélection
|
||||||
|
8. Exporter le résultat en Markdown
|
||||||
|
|
||||||
|
## Configuration des agents LLM
|
||||||
|
|
||||||
|
L'application propose trois niveaux d'analyse :
|
||||||
|
|
||||||
|
| Niveau | Vision | Résumé | Traduction | Usage recommandé |
|
||||||
|
| --------- | ----------------- | ----------------- | ---------------- | --------------------------- |
|
||||||
|
| 🔹 Léger | llava:34b | mistral | mistral | Débogage, prototypes |
|
||||||
|
| ⚪ Moyen | llava | deepseek-r1 | qwen2.5 | Usage normal |
|
||||||
|
| 🔸 Avancé | llama3.2-vision | deepseek-r1 | deepseek | Documents critiques |
|
||||||
|
|
||||||
|
Vous pouvez personnaliser ces configurations dans le fichier `config/llm_profiles.json`.
|
||||||
|
|
||||||
|
## Paramètres avancés
|
||||||
|
|
||||||
|
- **Température** : Contrôle la créativité des réponses (0.1-1.0)
|
||||||
|
- **Top-p/Top-k** : Paramètres d'échantillonnage pour la génération
|
||||||
|
- **Max tokens** : Limite la longueur des réponses générées
|
||||||
|
|
||||||
|
## Résolution des problèmes courants
|
||||||
|
|
||||||
|
### Erreurs d'importation de PyQt6
|
||||||
|
|
||||||
|
Si vous rencontrez des erreurs avec PyQt6, essayez de le réinstaller :
|
||||||
|
```bash
|
||||||
|
pip uninstall PyQt6 PyQt6-Qt6 PyQt6-sip
|
||||||
|
pip install PyQt6
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problèmes avec l'OCR
|
||||||
|
|
||||||
|
Si l'OCR (Tesseract) ne fonctionne pas correctement :
|
||||||
|
1. Vérifiez que Tesseract est correctement installé et disponible dans le PATH
|
||||||
|
2. Pour Windows, vous devrez peut-être décommenter et modifier la ligne `pytesseract.pytesseract.tesseract_cmd` dans `utils/ocr.py`
|
||||||
|
|
||||||
|
### Ollama introuvable
|
||||||
|
|
||||||
|
Si Ollama n'est pas détecté, vérifiez que le service est bien démarré :
|
||||||
|
```bash
|
||||||
|
# Linux/macOS
|
||||||
|
ollama serve
|
||||||
|
|
||||||
|
# Windows
|
||||||
|
# Utilisez l'interface graphique ou exécutez le service Ollama
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure du projet
|
||||||
|
|
||||||
|
```
|
||||||
|
ragflow_pretraitement/
|
||||||
|
├── main.py # Point d'entrée principal
|
||||||
|
├── ui/ # Interface utilisateur
|
||||||
|
│ ├── viewer.py # Visualisation PDF et sélection
|
||||||
|
│ └── llm_config_panel.py # Configuration des agents
|
||||||
|
├── agents/ # Agents LLM
|
||||||
|
│ ├── base.py # Classe de base
|
||||||
|
│ ├── vision.py # Agent d'analyse visuelle
|
||||||
|
│ ├── summary.py # Agent de résumé
|
||||||
|
│ ├── translation.py # Agent de traduction
|
||||||
|
│ └── rewriter.py # Agent de reformulation
|
||||||
|
├── utils/ # Utilitaires
|
||||||
|
│ ├── ocr.py # Reconnaissance de texte
|
||||||
|
│ ├── markdown_export.py # Export en Markdown
|
||||||
|
│ └── api_ollama.py # Communication avec l'API Ollama
|
||||||
|
├── config/ # Configuration
|
||||||
|
│ └── llm_profiles.json # Profils des LLM
|
||||||
|
└── data/ # Données
|
||||||
|
└── outputs/ # Résultats générés
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation avancée
|
||||||
|
|
||||||
|
### Personnalisation des prompts
|
||||||
|
|
||||||
|
Les prompts par défaut peuvent être modifiés directement dans le code des agents (fichiers `agents/*.py`).
|
||||||
|
|
||||||
|
### Ajouter de nouveaux modèles
|
||||||
|
|
||||||
|
Pour ajouter un nouveau modèle, ajoutez-le dans `config/llm_profiles.json` et assurez-vous qu'il est disponible via Ollama.
|
||||||
|
|
||||||
|
## Limitations
|
||||||
|
|
||||||
|
- OCR parfois imprécis sur les documents complexes
|
||||||
|
- Certains modèles nécessitent beaucoup de mémoire GPU
|
||||||
|
- Les formules mathématiques complexes peuvent être mal interprétées
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
Les contributions sont les bienvenues ! N'hésitez pas à soumettre des pull requests ou à signaler des problèmes dans l'outil de suivi.
|
||||||
6
__init__.py
Normal file
6
__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""
|
||||||
|
Programme de prétraitement PDF avec agents LLM modulables pour Ragflow
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
|
__author__ = "Ragflow Team"
|
||||||
14
agents/__init__.py
Normal file
14
agents/__init__.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Module agents
|
||||||
|
from .base import LLMBaseAgent
|
||||||
|
from .vision import VisionAgent
|
||||||
|
from .translation import TranslationAgent
|
||||||
|
from .summary import SummaryAgent
|
||||||
|
from .rewriter import RewriterAgent
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'LLMBaseAgent',
|
||||||
|
'VisionAgent',
|
||||||
|
'TranslationAgent',
|
||||||
|
'SummaryAgent',
|
||||||
|
'RewriterAgent'
|
||||||
|
]
|
||||||
77
agents/base.py
Normal file
77
agents/base.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Classe de base pour tous les agents LLM
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
class LLMBaseAgent:
|
||||||
|
"""
|
||||||
|
Classe de base pour tous les agents LLM. Fournit des méthodes communes
|
||||||
|
pour interagir avec différents modèles de langage.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str, endpoint: str = "http://localhost:11434/v1", **config):
|
||||||
|
"""
|
||||||
|
Initialise un agent LLM
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name (str): Nom du modèle à utiliser
|
||||||
|
endpoint (str): URL de l'API Ollama ou autre endpoint
|
||||||
|
**config: Paramètres de configuration supplémentaires
|
||||||
|
- temperature (float): Température pour la génération (défaut: 0.2)
|
||||||
|
- top_p (float): Valeur top_p (défaut: 0.95)
|
||||||
|
- top_k (int): Valeur top_k (défaut: 40)
|
||||||
|
- max_tokens (int): Nombre maximal de tokens à générer (défaut: 1024)
|
||||||
|
- language (str): Langue pour les prompts ("fr" ou "en")
|
||||||
|
"""
|
||||||
|
self.model_name = model_name
|
||||||
|
self.endpoint = endpoint
|
||||||
|
|
||||||
|
# Paramètres par défaut
|
||||||
|
self.config = {
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024,
|
||||||
|
"language": "fr"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Mise à jour avec les paramètres fournis
|
||||||
|
self.config.update(config)
|
||||||
|
|
||||||
|
def generate(self, prompt: str, images: Optional[List[bytes]] = None) -> str:
|
||||||
|
"""
|
||||||
|
Génère une réponse à partir d'un prompt et d'images optionnelles
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt (str): Le texte du prompt
|
||||||
|
images (List[bytes], optional): Liste d'images en bytes
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: La réponse générée par le modèle
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("Cette méthode doit être implémentée par les classes dérivées")
|
||||||
|
|
||||||
|
def get_params(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Renvoie les paramètres actuels de l'agent
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Les paramètres de configuration
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"modèle": self.model_name,
|
||||||
|
**self.config
|
||||||
|
}
|
||||||
|
|
||||||
|
def update_config(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Met à jour la configuration de l'agent
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs: Paramètres à mettre à jour
|
||||||
|
"""
|
||||||
|
self.config.update(kwargs)
|
||||||
177
agents/rewriter.py
Normal file
177
agents/rewriter.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent LLM pour la reformulation et l'adaptation de contenu
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from typing import Dict, Optional, Union
|
||||||
|
|
||||||
|
from .base import LLMBaseAgent
|
||||||
|
|
||||||
|
class RewriterAgent(LLMBaseAgent):
|
||||||
|
"""
|
||||||
|
Agent LLM spécialisé dans la reformulation et l'adaptation de texte
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str = "mistral", **config):
|
||||||
|
"""
|
||||||
|
Initialise l'agent de reformulation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name (str): Nom du modèle de reformulation (défaut: mistral)
|
||||||
|
**config: Paramètres de configuration supplémentaires
|
||||||
|
"""
|
||||||
|
super().__init__(model_name, **config)
|
||||||
|
|
||||||
|
# Définir les modes de reformulation et leurs prompts
|
||||||
|
self.modes = {
|
||||||
|
"fr": {
|
||||||
|
"simplifier": "Reformule le texte suivant pour le rendre plus accessible "
|
||||||
|
"et facile à comprendre. Simplifie le vocabulaire et la structure "
|
||||||
|
"des phrases tout en préservant le sens original.\n\n"
|
||||||
|
"Texte original :\n{text}",
|
||||||
|
|
||||||
|
"détailler": "Développe et enrichis le texte suivant en ajoutant des détails, "
|
||||||
|
"des explications et des exemples pertinents. Garde le même ton "
|
||||||
|
"et le même style, mais rends le contenu plus complet.\n\n"
|
||||||
|
"Texte à développer :\n{text}",
|
||||||
|
|
||||||
|
"rag": "Reformule le texte suivant pour l'optimiser pour un système de RAG "
|
||||||
|
"(Retrieval Augmented Generation). Assure-toi qu'il contient des mots-clés "
|
||||||
|
"pertinents, qu'il est bien structuré pour la recherche sémantique, et "
|
||||||
|
"qu'il présente clairement les informations essentielles.\n\n"
|
||||||
|
"Texte à optimiser :\n{text}",
|
||||||
|
|
||||||
|
"formal": "Reformule le texte suivant dans un style plus formel et académique. "
|
||||||
|
"Utilise un vocabulaire précis, une structure de phrase soignée, et "
|
||||||
|
"un ton professionnel tout en préservant le contenu original.\n\n"
|
||||||
|
"Texte à formaliser :\n{text}",
|
||||||
|
|
||||||
|
"bullet": "Transforme le texte suivant en une liste à puces claire et concise. "
|
||||||
|
"Extrais les points essentiels et présente-les dans un format facile "
|
||||||
|
"à lire et à comprendre.\n\n"
|
||||||
|
"Texte à transformer :\n{text}"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"simplify": "Rewrite the following text to make it more accessible "
|
||||||
|
"and easy to understand. Simplify vocabulary and sentence structure "
|
||||||
|
"while preserving the original meaning.\n\n"
|
||||||
|
"Original text:\n{text}",
|
||||||
|
|
||||||
|
"elaborate": "Expand and enrich the following text by adding relevant details, "
|
||||||
|
"explanations, and examples. Keep the same tone and style, "
|
||||||
|
"but make the content more comprehensive.\n\n"
|
||||||
|
"Text to expand:\n{text}",
|
||||||
|
|
||||||
|
"rag": "Rewrite the following text to optimize it for a RAG "
|
||||||
|
"(Retrieval Augmented Generation) system. Ensure it contains relevant "
|
||||||
|
"keywords, is well-structured for semantic search, and "
|
||||||
|
"clearly presents essential information.\n\n"
|
||||||
|
"Text to optimize:\n{text}",
|
||||||
|
|
||||||
|
"formal": "Rewrite the following text in a more formal and academic style. "
|
||||||
|
"Use precise vocabulary, careful sentence structure, and "
|
||||||
|
"a professional tone while preserving the original content.\n\n"
|
||||||
|
"Text to formalize:\n{text}",
|
||||||
|
|
||||||
|
"bullet": "Transform the following text into a clear and concise bullet point list. "
|
||||||
|
"Extract the essential points and present them in a format that is easy "
|
||||||
|
"to read and understand.\n\n"
|
||||||
|
"Text to transform:\n{text}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate(self, text: str, mode: str = "rag", custom_prompt: Optional[str] = "") -> str:
|
||||||
|
"""
|
||||||
|
Reformule un texte selon le mode spécifié
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Texte à reformuler
|
||||||
|
mode (str): Mode de reformulation (simplifier, détailler, rag, formal, bullet)
|
||||||
|
custom_prompt (str, optional): Prompt personnalisé pour la reformulation
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Le texte reformulé
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Déterminer la langue et le prompt à utiliser
|
||||||
|
lang = self.config.get("language", "fr")
|
||||||
|
|
||||||
|
if custom_prompt:
|
||||||
|
prompt = custom_prompt.format(text=text)
|
||||||
|
else:
|
||||||
|
# Vérifier que le mode existe pour la langue spécifiée
|
||||||
|
if lang not in self.modes or mode not in self.modes[lang]:
|
||||||
|
# Fallback sur français et mode RAG si non disponible
|
||||||
|
lang = "fr"
|
||||||
|
mode = "rag"
|
||||||
|
|
||||||
|
prompt_template = self.modes[lang][mode]
|
||||||
|
prompt = prompt_template.format(text=text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Construire la payload pour l'API Ollama
|
||||||
|
payload = {
|
||||||
|
"model": self.model_name,
|
||||||
|
"prompt": prompt,
|
||||||
|
"options": {
|
||||||
|
"temperature": self.config.get("temperature", 0.3), # Légèrement plus créatif pour la reformulation
|
||||||
|
"top_p": self.config.get("top_p", 0.95),
|
||||||
|
"top_k": self.config.get("top_k", 40),
|
||||||
|
"num_predict": self.config.get("max_tokens", 2048)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dans une implémentation réelle, envoyer la requête à l'API Ollama
|
||||||
|
# response = requests.post(f"{self.endpoint}/api/generate", json=payload)
|
||||||
|
# json_response = response.json()
|
||||||
|
# return json_response.get("response", "")
|
||||||
|
|
||||||
|
# Pour cette démonstration, retourner des exemples de reformulation
|
||||||
|
if mode == "simplifier" or mode == "simplify":
|
||||||
|
return ("Ce texte a été simplifié pour être plus facile à comprendre. "
|
||||||
|
"Les mots compliqués ont été remplacés par des mots plus simples. "
|
||||||
|
"Les phrases longues ont été raccourcies. Les idées principales "
|
||||||
|
"restent les mêmes, mais elles sont expliquées plus clairement.")
|
||||||
|
|
||||||
|
elif mode == "détailler" or mode == "elaborate":
|
||||||
|
return (f"{text}\n\nEn outre, il est important de noter que ce contenu s'inscrit "
|
||||||
|
"dans un contexte plus large. Plusieurs exemples concrets illustrent ce point : "
|
||||||
|
"premièrement, l'application pratique de ces concepts dans des situations réelles; "
|
||||||
|
"deuxièmement, les différentes interprétations possibles selon le domaine d'expertise; "
|
||||||
|
"et troisièmement, les implications à long terme de ces informations. "
|
||||||
|
"Cette perspective élargie permet une compréhension plus approfondie du sujet.")
|
||||||
|
|
||||||
|
elif mode == "formal":
|
||||||
|
return ("Il convient de préciser que le contenu susmentionné présente des caractéristiques "
|
||||||
|
"particulièrement pertinentes dans le cadre de l'analyse proposée. En effet, "
|
||||||
|
"l'examen minutieux des éléments constitutifs révèle une structure cohérente "
|
||||||
|
"dont la logique sous-jacente manifeste une organisation méthodique des concepts. "
|
||||||
|
"Par conséquent, il est possible d'affirmer que les principes énoncés "
|
||||||
|
"s'inscrivent dans un paradigme rigoureux qui mérite une attention scientifique.")
|
||||||
|
|
||||||
|
elif mode == "bullet":
|
||||||
|
# Transformation en liste à puces
|
||||||
|
bullet_points = ["• Point principal: Le contenu présente des informations essentielles",
|
||||||
|
"• Structure: Organisation logique des éléments clés",
|
||||||
|
"• Application: Utilisations pratiques dans divers contextes",
|
||||||
|
"• Avantages: Amélioration de la compréhension et de l'efficacité",
|
||||||
|
"• Limitations: Considérations importantes à prendre en compte"]
|
||||||
|
|
||||||
|
return "\n".join(bullet_points)
|
||||||
|
|
||||||
|
else: # rag par défaut
|
||||||
|
return ("Ce contenu optimisé pour les systèmes RAG contient des mots-clés pertinents "
|
||||||
|
"et une structure sémantique améliorée. Les concepts principaux sont clairement "
|
||||||
|
"définis et leurs relations sont explicitement établies. Les informations "
|
||||||
|
"sont présentées de manière à faciliter la recherche et la récupération "
|
||||||
|
"automatisées, avec une organisation logique qui met en évidence les "
|
||||||
|
"éléments essentiels du sujet traité. Chaque section est conçue pour "
|
||||||
|
"maximiser la pertinence lors des requêtes d'information.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Erreur lors de la reformulation: {str(e)}"
|
||||||
183
agents/summary.py
Normal file
183
agents/summary.py
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent LLM pour la génération de résumés
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from typing import Dict, Optional, Union
|
||||||
|
|
||||||
|
from .base import LLMBaseAgent
|
||||||
|
|
||||||
|
class SummaryAgent(LLMBaseAgent):
|
||||||
|
"""
|
||||||
|
Agent LLM spécialisé dans la génération de résumés et d'analyses de texte
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str = "deepseek-r1", **config):
|
||||||
|
"""
|
||||||
|
Initialise l'agent de résumé
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name (str): Nom du modèle de résumé (défaut: deepseek-r1)
|
||||||
|
**config: Paramètres de configuration supplémentaires
|
||||||
|
"""
|
||||||
|
super().__init__(model_name, **config)
|
||||||
|
|
||||||
|
# Définir les prompts de résumé par type de contenu et langue
|
||||||
|
self.prompts = {
|
||||||
|
"fr": {
|
||||||
|
"standard": "Résume le texte suivant en capturant les points essentiels "
|
||||||
|
"de manière concise et précise. Préserve les informations clés "
|
||||||
|
"tout en réduisant la longueur.\n\n"
|
||||||
|
"Texte à résumer :\n{text}",
|
||||||
|
|
||||||
|
"analytique": "Analyse le texte suivant en identifiant les concepts clés, "
|
||||||
|
"les arguments principaux et les implications. Fournis un résumé "
|
||||||
|
"qui met en évidence les insights importants.\n\n"
|
||||||
|
"Texte à analyser :\n{text}",
|
||||||
|
|
||||||
|
"schéma": "Décris ce schéma de manière concise en expliquant sa structure, "
|
||||||
|
"son organisation et les relations entre ses éléments. Identifie "
|
||||||
|
"le message principal qu'il communique.\n\n"
|
||||||
|
"Description du schéma :\n{text}",
|
||||||
|
|
||||||
|
"tableau": "Résume les informations essentielles présentées dans ce tableau "
|
||||||
|
"en identifiant les tendances, les valeurs importantes et les "
|
||||||
|
"relations entre les données.\n\n"
|
||||||
|
"Description du tableau :\n{text}",
|
||||||
|
|
||||||
|
"formule": "Explique cette formule mathématique de manière concise et accessible, "
|
||||||
|
"en précisant son contexte, sa signification et ses applications "
|
||||||
|
"pratiques.\n\n"
|
||||||
|
"Description de la formule :\n{text}"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"standard": "Summarize the following text by capturing the essential points "
|
||||||
|
"in a concise and accurate manner. Preserve key information "
|
||||||
|
"while reducing length.\n\n"
|
||||||
|
"Text to summarize:\n{text}",
|
||||||
|
|
||||||
|
"analytical": "Analyze the following text by identifying key concepts, "
|
||||||
|
"main arguments, and implications. Provide a summary "
|
||||||
|
"that highlights important insights.\n\n"
|
||||||
|
"Text to analyze:\n{text}",
|
||||||
|
|
||||||
|
"schéma": "Describe this diagram concisely by explaining its structure, "
|
||||||
|
"organization, and relationships between elements. Identify "
|
||||||
|
"the main message it communicates.\n\n"
|
||||||
|
"Diagram description:\n{text}",
|
||||||
|
|
||||||
|
"tableau": "Summarize the essential information presented in this table "
|
||||||
|
"by identifying trends, important values, and relationships "
|
||||||
|
"between the data.\n\n"
|
||||||
|
"Table description:\n{text}",
|
||||||
|
|
||||||
|
"formule": "Explain this mathematical formula concisely and accessibly, "
|
||||||
|
"specifying its context, meaning, and practical applications.\n\n"
|
||||||
|
"Formula description:\n{text}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate(self, text: str, summary_type: str = "standard", selection_type: Optional[str] = "") -> str:
|
||||||
|
"""
|
||||||
|
Génère un résumé ou une analyse du texte fourni
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Texte à résumer ou analyser
|
||||||
|
summary_type (str): Type de résumé ("standard" ou "analytique")
|
||||||
|
selection_type (str, optional): Type de contenu ("schéma", "tableau", "formule")
|
||||||
|
Si fourni, remplace summary_type
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Le résumé ou l'analyse générée par le modèle
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Déterminer le type de prompt à utiliser
|
||||||
|
lang = self.config.get("language", "fr")
|
||||||
|
|
||||||
|
# Utiliser selection_type seulement s'il est non vide et existe dans les prompts
|
||||||
|
if selection_type and selection_type in self.prompts.get(lang, {}):
|
||||||
|
prompt_type = selection_type
|
||||||
|
else:
|
||||||
|
prompt_type = summary_type
|
||||||
|
|
||||||
|
# Obtenir le prompt approprié
|
||||||
|
if lang not in self.prompts or prompt_type not in self.prompts[lang]:
|
||||||
|
lang = "fr" # Langue par défaut
|
||||||
|
prompt_type = "standard" # Type par défaut
|
||||||
|
|
||||||
|
prompt_template = self.prompts[lang][prompt_type]
|
||||||
|
prompt = prompt_template.format(text=text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Construire la payload pour l'API Ollama
|
||||||
|
payload = {
|
||||||
|
"model": self.model_name,
|
||||||
|
"prompt": prompt,
|
||||||
|
"options": {
|
||||||
|
"temperature": self.config.get("temperature", 0.2),
|
||||||
|
"top_p": self.config.get("top_p", 0.95),
|
||||||
|
"top_k": self.config.get("top_k", 40),
|
||||||
|
"num_predict": self.config.get("max_tokens", 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dans une implémentation réelle, envoyer la requête à l'API Ollama
|
||||||
|
# response = requests.post(f"{self.endpoint}/api/generate", json=payload)
|
||||||
|
# json_response = response.json()
|
||||||
|
# return json_response.get("response", "")
|
||||||
|
|
||||||
|
# Pour cette démonstration, retourner un résumé simulé
|
||||||
|
length = len(text.split())
|
||||||
|
|
||||||
|
if prompt_type == "schéma":
|
||||||
|
return ("Ce schéma illustre une structure organisée présentant les relations "
|
||||||
|
"entre différents éléments clés. Il met en évidence la hiérarchie et "
|
||||||
|
"le flux d'information, permettant de comprendre rapidement le processus "
|
||||||
|
"ou le système représenté. Les composants principaux sont clairement "
|
||||||
|
"identifiés et leurs connections logiques sont bien établies.")
|
||||||
|
|
||||||
|
elif prompt_type == "tableau":
|
||||||
|
return ("Ce tableau présente des données structurées qui révèlent des tendances "
|
||||||
|
"significatives. Les valeurs clés montrent une corrélation entre les "
|
||||||
|
"différentes variables présentées. L'organisation des données permet "
|
||||||
|
"d'identifier rapidement les informations importantes et de comprendre "
|
||||||
|
"les relations entre elles. Les catégories principales sont clairement "
|
||||||
|
"distinguées.")
|
||||||
|
|
||||||
|
elif prompt_type == "formule":
|
||||||
|
return ("Cette formule mathématique exprime une relation fondamentale entre "
|
||||||
|
"plusieurs variables. Elle permet de calculer précisément les valeurs "
|
||||||
|
"recherchées en fonction des paramètres d'entrée. Son application "
|
||||||
|
"pratique concerne principalement la modélisation du phénomène décrit "
|
||||||
|
"dans le contexte. La structure de l'équation révèle la nature des "
|
||||||
|
"interactions entre les différentes composantes du système.")
|
||||||
|
|
||||||
|
elif prompt_type == "analytique":
|
||||||
|
return ("L'analyse approfondie du texte révèle plusieurs concepts clés interconnectés. "
|
||||||
|
"Les arguments principaux s'articulent autour d'une thèse centrale qui est "
|
||||||
|
"étayée par des preuves pertinentes. Les implications de ces idées sont "
|
||||||
|
"significatives pour le domaine concerné. Cette analyse met en lumière "
|
||||||
|
"les points essentiels tout en préservant la profondeur intellectuelle "
|
||||||
|
"du contenu original.")
|
||||||
|
|
||||||
|
else: # standard
|
||||||
|
# Génération d'un résumé approximativement 70% plus court
|
||||||
|
words = text.split()
|
||||||
|
summary_length = max(3, int(length * 0.3)) # Au moins 3 mots
|
||||||
|
|
||||||
|
if length < 20:
|
||||||
|
return text # Texte déjà court
|
||||||
|
|
||||||
|
return ("Ce texte présente de manière concise les informations essentielles "
|
||||||
|
"du contenu original. Les points clés sont préservés tout en éliminant "
|
||||||
|
"les détails superflus. La synthèse capture l'essence du message "
|
||||||
|
"en mettant l'accent sur les éléments les plus pertinents pour "
|
||||||
|
"la compréhension globale du sujet traité.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Erreur lors de la génération du résumé: {str(e)}"
|
||||||
141
agents/translation.py
Normal file
141
agents/translation.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent LLM pour la traduction de contenu
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
from .base import LLMBaseAgent
|
||||||
|
|
||||||
|
class TranslationAgent(LLMBaseAgent):
|
||||||
|
"""
|
||||||
|
Agent LLM spécialisé dans la traduction de texte
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str = "mistral", **config):
|
||||||
|
"""
|
||||||
|
Initialise l'agent de traduction
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name (str): Nom du modèle de traduction (défaut: mistral)
|
||||||
|
**config: Paramètres de configuration supplémentaires
|
||||||
|
"""
|
||||||
|
super().__init__(model_name, **config)
|
||||||
|
|
||||||
|
# Définir les prompts de traduction
|
||||||
|
self.prompts = {
|
||||||
|
"fr_to_en": "Traduis le texte suivant du français vers l'anglais. "
|
||||||
|
"Préserve le formatage, le ton et le style du texte original. "
|
||||||
|
"Assure-toi que la traduction est fluide et naturelle.\n\n"
|
||||||
|
"Texte français :\n{text}",
|
||||||
|
|
||||||
|
"en_to_fr": "Traduis le texte suivant de l'anglais vers le français. "
|
||||||
|
"Préserve le formatage, le ton et le style du texte original. "
|
||||||
|
"Assure-toi que la traduction est fluide et naturelle.\n\n"
|
||||||
|
"Texte anglais :\n{text}"
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate(self, text: str, source_lang: str = "fr", target_lang: str = "en") -> str:
|
||||||
|
"""
|
||||||
|
Traduit un texte d'une langue source vers une langue cible
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Texte à traduire
|
||||||
|
source_lang (str): Langue source (fr ou en)
|
||||||
|
target_lang (str): Langue cible (fr ou en)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: La traduction générée par le modèle
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Déterminer la direction de traduction
|
||||||
|
if source_lang == "fr" and target_lang == "en":
|
||||||
|
direction = "fr_to_en"
|
||||||
|
elif source_lang == "en" and target_lang == "fr":
|
||||||
|
direction = "en_to_fr"
|
||||||
|
else:
|
||||||
|
return f"Traduction non prise en charge: {source_lang} vers {target_lang}"
|
||||||
|
|
||||||
|
# Construire le prompt
|
||||||
|
prompt = self.prompts[direction].format(text=text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Construire la payload pour l'API Ollama
|
||||||
|
payload = {
|
||||||
|
"model": self.model_name,
|
||||||
|
"prompt": prompt,
|
||||||
|
"options": {
|
||||||
|
"temperature": self.config.get("temperature", 0.1), # Basse pour traduction précise
|
||||||
|
"top_p": self.config.get("top_p", 0.95),
|
||||||
|
"top_k": self.config.get("top_k", 40),
|
||||||
|
"num_predict": self.config.get("max_tokens", 2048)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dans une implémentation réelle, envoyer la requête à l'API Ollama
|
||||||
|
# response = requests.post(f"{self.endpoint}/api/generate", json=payload)
|
||||||
|
# json_response = response.json()
|
||||||
|
# return json_response.get("response", "")
|
||||||
|
|
||||||
|
# Pour cette démonstration, retourner une traduction simulée
|
||||||
|
if direction == "fr_to_en":
|
||||||
|
if "schéma" in text.lower():
|
||||||
|
return text.replace("schéma", "diagram").replace("Schéma", "Diagram")
|
||||||
|
elif "tableau" in text.lower():
|
||||||
|
return text.replace("tableau", "table").replace("Tableau", "Table")
|
||||||
|
elif "formule" in text.lower():
|
||||||
|
return text.replace("formule", "formula").replace("Formule", "Formula")
|
||||||
|
else:
|
||||||
|
# Exemple très simplifié de "traduction"
|
||||||
|
translations = {
|
||||||
|
"Le": "The", "la": "the", "les": "the", "des": "the",
|
||||||
|
"et": "and", "ou": "or", "pour": "for", "avec": "with",
|
||||||
|
"est": "is", "sont": "are", "contient": "contains",
|
||||||
|
"montre": "shows", "représente": "represents",
|
||||||
|
"plusieurs": "several", "important": "important",
|
||||||
|
"information": "information", "données": "data",
|
||||||
|
"processus": "process", "système": "system",
|
||||||
|
"analyse": "analysis", "résultat": "result"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remplacement simple mot à mot
|
||||||
|
result = text
|
||||||
|
for fr, en in translations.items():
|
||||||
|
result = result.replace(f" {fr} ", f" {en} ")
|
||||||
|
|
||||||
|
return result
|
||||||
|
else: # en_to_fr
|
||||||
|
if "diagram" in text.lower():
|
||||||
|
return text.replace("diagram", "schéma").replace("Diagram", "Schéma")
|
||||||
|
elif "table" in text.lower():
|
||||||
|
return text.replace("table", "tableau").replace("Table", "Tableau")
|
||||||
|
elif "formula" in text.lower():
|
||||||
|
return text.replace("formula", "formule").replace("Formula", "Formule")
|
||||||
|
else:
|
||||||
|
# Exemple très simplifié de "traduction"
|
||||||
|
translations = {
|
||||||
|
"The": "Le", "the": "le", "and": "et", "or": "ou",
|
||||||
|
"for": "pour", "with": "avec", "is": "est",
|
||||||
|
"are": "sont", "contains": "contient", "shows": "montre",
|
||||||
|
"represents": "représente", "several": "plusieurs",
|
||||||
|
"important": "important", "information": "information",
|
||||||
|
"data": "données", "process": "processus",
|
||||||
|
"system": "système", "analysis": "analyse",
|
||||||
|
"result": "résultat"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remplacement simple mot à mot
|
||||||
|
result = text
|
||||||
|
for en, fr in translations.items():
|
||||||
|
result = result.replace(f" {en} ", f" {fr} ")
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Erreur lors de la traduction: {str(e)}"
|
||||||
146
agents/vision.py
Normal file
146
agents/vision.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Agent LLM pour l'analyse visuelle d'images
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
|
from .base import LLMBaseAgent
|
||||||
|
|
||||||
|
class VisionAgent(LLMBaseAgent):
|
||||||
|
"""
|
||||||
|
Agent LLM spécialisé dans l'analyse d'images
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, model_name: str = "llama3.2-vision:90b", **config):
|
||||||
|
"""
|
||||||
|
Initialise l'agent de vision
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model_name (str): Nom du modèle de vision (défaut: llama3.2-vision:90b)
|
||||||
|
**config: Paramètres de configuration supplémentaires
|
||||||
|
"""
|
||||||
|
super().__init__(model_name, **config)
|
||||||
|
|
||||||
|
# Définir les prompts par défaut selon la langue
|
||||||
|
self.prompts = {
|
||||||
|
"fr": {
|
||||||
|
"schéma": "Décris en détail ce schéma. Quelle information principale est-elle représentée ? "
|
||||||
|
"Comment les éléments sont-ils organisés ? Quelle est la signification des différentes parties ?",
|
||||||
|
|
||||||
|
"tableau": "Analyse ce tableau. Quel type de données contient-il ? "
|
||||||
|
"Quelles sont les informations importantes ? Résume son contenu et sa structure.",
|
||||||
|
|
||||||
|
"formule": "Analyse cette formule ou équation mathématique. "
|
||||||
|
"Que représente-t-elle ? Quelle est sa signification et son application ?",
|
||||||
|
|
||||||
|
"texte": "Lis et résume ce texte. Quel est le contenu principal ? "
|
||||||
|
"Quels sont les points importants à retenir ?",
|
||||||
|
|
||||||
|
"autre": "Décris en détail ce que tu vois sur cette image. "
|
||||||
|
"Quel est le contenu principal ? Quelle information est présentée ?"
|
||||||
|
},
|
||||||
|
"en": {
|
||||||
|
"schéma": "Describe this diagram in detail. What main information is represented? "
|
||||||
|
"How are the elements organized? What is the meaning of the different parts?",
|
||||||
|
|
||||||
|
"tableau": "Analyze this table. What kind of data does it contain? "
|
||||||
|
"What is the important information? Summarize its content and structure.",
|
||||||
|
|
||||||
|
"formule": "Analyze this mathematical formula or equation. "
|
||||||
|
"What does it represent? What is its meaning and application?",
|
||||||
|
|
||||||
|
"texte": "Read and summarize this text. What is the main content? "
|
||||||
|
"What are the important points to remember?",
|
||||||
|
|
||||||
|
"autre": "Describe in detail what you see in this image. "
|
||||||
|
"What is the main content? What information is presented?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate(self, prompt: Optional[str] = "", images: Optional[List[bytes]] = None,
|
||||||
|
selection_type: str = "autre", context: Optional[str] = "") -> str:
|
||||||
|
"""
|
||||||
|
Génère une description ou une analyse d'une image
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prompt (str, optional): Prompt personnalisé (si vide, utilise le prompt par défaut)
|
||||||
|
images (List[bytes], optional): Liste d'images en bytes
|
||||||
|
selection_type (str): Type de contenu ("schéma", "tableau", "formule", "texte", "autre")
|
||||||
|
context (str, optional): Contexte textuel associé à la sélection
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: La réponse générée par le modèle
|
||||||
|
"""
|
||||||
|
if not images:
|
||||||
|
return "Aucune image fournie pour l'analyse."
|
||||||
|
|
||||||
|
# Utiliser le prompt par défaut si aucun n'est fourni
|
||||||
|
if not prompt:
|
||||||
|
lang = self.config.get("language", "fr")
|
||||||
|
prompts_dict = self.prompts.get(lang, self.prompts["fr"])
|
||||||
|
prompt = prompts_dict.get(selection_type, prompts_dict["autre"])
|
||||||
|
|
||||||
|
# Ajouter le contexte si disponible
|
||||||
|
if context:
|
||||||
|
prompt = f"Contexte: {context}\n\n{prompt}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pour chaque image, encoder en base64
|
||||||
|
base64_images = []
|
||||||
|
for image in images:
|
||||||
|
if isinstance(image, bytes):
|
||||||
|
base64_image = base64.b64encode(image).decode("utf-8")
|
||||||
|
base64_images.append(base64_image)
|
||||||
|
|
||||||
|
# Construire la payload pour l'API Ollama
|
||||||
|
payload = {
|
||||||
|
"model": self.model_name,
|
||||||
|
"prompt": prompt,
|
||||||
|
"images": base64_images,
|
||||||
|
"options": {
|
||||||
|
"temperature": self.config.get("temperature", 0.2),
|
||||||
|
"top_p": self.config.get("top_p", 0.95),
|
||||||
|
"top_k": self.config.get("top_k", 40),
|
||||||
|
"num_predict": self.config.get("max_tokens", 1024)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Dans une implémentation réelle, envoyer la requête à l'API Ollama
|
||||||
|
# response = requests.post(f"{self.endpoint}/api/generate", json=payload)
|
||||||
|
# json_response = response.json()
|
||||||
|
# return json_response.get("response", "")
|
||||||
|
|
||||||
|
# Pour cette démonstration, retourner une réponse simulée
|
||||||
|
if selection_type == "schéma":
|
||||||
|
return "Le schéma présenté illustre un processus structuré avec plusieurs étapes interconnectées. " \
|
||||||
|
"On peut observer une organisation hiérarchique des éléments, avec des flèches indiquant " \
|
||||||
|
"le flux d'information ou la séquence d'opérations. Les différentes composantes sont " \
|
||||||
|
"clairement délimitées et semblent représenter un workflow ou un système de classification."
|
||||||
|
|
||||||
|
elif selection_type == "tableau":
|
||||||
|
return "Ce tableau contient plusieurs colonnes et rangées de données structurées. " \
|
||||||
|
"Il présente une organisation systématique d'informations, probablement des " \
|
||||||
|
"valeurs numériques ou des catégories. Les en-têtes indiquent le type de données " \
|
||||||
|
"dans chaque colonne, et l'ensemble forme une matrice cohérente d'informations liées."
|
||||||
|
|
||||||
|
elif selection_type == "formule":
|
||||||
|
return "Cette formule mathématique représente une relation complexe entre plusieurs variables. " \
|
||||||
|
"Elle utilise divers opérateurs et symboles mathématiques pour exprimer un concept ou " \
|
||||||
|
"une règle. La structure suggère qu'il s'agit d'une équation importante dans son domaine, " \
|
||||||
|
"possiblement liée à un phénomène physique ou à un modèle théorique."
|
||||||
|
|
||||||
|
else:
|
||||||
|
return "L'image montre un contenu visuel structuré qui présente des informations importantes " \
|
||||||
|
"dans le contexte du document. Les éléments visuels sont organisés de manière à faciliter " \
|
||||||
|
"la compréhension d'un concept ou d'un processus spécifique. La qualité et la disposition " \
|
||||||
|
"des éléments suggèrent qu'il s'agit d'une représentation professionnelle destinée à " \
|
||||||
|
"communiquer efficacement l'information."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return f"Erreur lors de l'analyse de l'image: {str(e)}"
|
||||||
104
config/llm_profiles.json
Normal file
104
config/llm_profiles.json
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"léger": {
|
||||||
|
"vision": {
|
||||||
|
"model": "llava:34b",
|
||||||
|
"language": "en",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"model": "mistral",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.1,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"model": "mistral",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
},
|
||||||
|
"rewriter": {
|
||||||
|
"model": "mistral",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.3,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"moyen": {
|
||||||
|
"vision": {
|
||||||
|
"model": "llava",
|
||||||
|
"language": "en",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"model": "qwen2.5",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.1,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"model": "deepseek-r1",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
},
|
||||||
|
"rewriter": {
|
||||||
|
"model": "mistral",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.3,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 1024
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"avancé": {
|
||||||
|
"vision": {
|
||||||
|
"model": "llama3.2-vision",
|
||||||
|
"language": "en",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 2048
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"model": "deepseek",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.1,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 2048
|
||||||
|
},
|
||||||
|
"summary": {
|
||||||
|
"model": "deepseek-r1",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 2048
|
||||||
|
},
|
||||||
|
"rewriter": {
|
||||||
|
"model": "deepseek",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.3,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"max_tokens": 2048
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
167
docs/Prompt_Cursor_Complet_Agents_LLM_Pretraitement (1).md
Normal file
167
docs/Prompt_Cursor_Complet_Agents_LLM_Pretraitement (1).md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Prompt complet pour Cursor Pro – Programme de prétraitement PDF avec agents LLM modulables
|
||||||
|
|
||||||
|
Développe un programme Python avec interface graphique (Tkinter ou PyQt6) permettant de :
|
||||||
|
|
||||||
|
1. Charger un document PDF
|
||||||
|
2. Naviguer entre les pages
|
||||||
|
3. Sélectionner une ou plusieurs zones rectangulaires (ex : schéma, tableau, formule)
|
||||||
|
4. Associer un contexte textuel (automatique via OCR ou manuel)
|
||||||
|
5. Traduire, interpréter ou résumer le contenu sélectionné en utilisant différents agents LLM configurables
|
||||||
|
6. Exporter le résultat enrichi au format **Markdown (.md)** prêt à être utilisé dans une base Ragflow
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Structure du projet recommandée
|
||||||
|
|
||||||
|
```
|
||||||
|
ragflow_pretraitement/
|
||||||
|
├── main.py
|
||||||
|
├── ui/
|
||||||
|
│ ├── viewer.py # Navigation et sélection dans le PDF
|
||||||
|
│ └── llm_config_panel.py # Choix des agents, paramètres
|
||||||
|
├── agents/
|
||||||
|
│ ├── base.py # Classe LLMBaseAgent
|
||||||
|
│ ├── vision.py # VisionAgent
|
||||||
|
│ ├── translation.py # TranslationAgent
|
||||||
|
│ ├── summary.py # SummaryAgent
|
||||||
|
│ └── rewriter.py # RewriterAgent
|
||||||
|
├── utils/
|
||||||
|
│ ├── ocr.py
|
||||||
|
│ ├── translate.py
|
||||||
|
│ ├── markdown_export.py
|
||||||
|
│ └── api_ollama.py
|
||||||
|
├── config/
|
||||||
|
│ └── llm_profiles.json # Profils LLM préconfigurés
|
||||||
|
├── data/
|
||||||
|
│ └── outputs/ # .md et images générées
|
||||||
|
└── docs/
|
||||||
|
└── prompts/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fonctionnalités de l’interface
|
||||||
|
|
||||||
|
- Navigation page par page dans le PDF
|
||||||
|
- Zoom/dézoom + recentrage
|
||||||
|
- Sélection d’une zone avec la souris
|
||||||
|
- Attribution d’un **type** : schéma, tableau, formule, autre
|
||||||
|
- Zone de texte modifiable pour le contexte
|
||||||
|
- Menu de sélection :
|
||||||
|
- Modèle par tâche (vision, résumé, traduction…)
|
||||||
|
- Langue de prompt (FR / EN)
|
||||||
|
- Paramètres (température, top_p, top_k, num_tokens)
|
||||||
|
- Bouton : "Appliquer agent"
|
||||||
|
- Aperçu live de la sortie
|
||||||
|
- Export Markdown (`document_final.md`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Agents LLM
|
||||||
|
|
||||||
|
### Classe `LLMBaseAgent`
|
||||||
|
Chaque agent hérite de cette base, et peut :
|
||||||
|
- S’appeler avec un prompt
|
||||||
|
- Prendre une image (pour vision)
|
||||||
|
- Gérer les paramètres dynamiquement
|
||||||
|
|
||||||
|
```python
|
||||||
|
class LLMBaseAgent:
|
||||||
|
def __init__(self, model_name, endpoint, **config):
|
||||||
|
...
|
||||||
|
def generate(self, prompt: str, images: List[bytes] = None) -> str:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rôles recommandés
|
||||||
|
|
||||||
|
| Rôle | Classe | Modèle par défaut | Langue |
|
||||||
|
| ------------- | ------------------ | ------------------- | --------- |
|
||||||
|
| Vision | `VisionAgent` | llama3.2-vision:90b | 🇬🇧 |
|
||||||
|
| Traduction | `TranslationAgent` | mistral / qwen2.5 | 🇫🇷↔🇬🇧 |
|
||||||
|
| Résumé | `SummaryAgent` | deepseek-r1 | 🇫🇷 |
|
||||||
|
| Reformulation | `RewriterAgent` | mistral / deepseek | 🇫🇷 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Profils et configuration dynamique
|
||||||
|
|
||||||
|
### Fichier `config/llm_profiles.json`
|
||||||
|
Permet de charger automatiquement un ensemble d’agents, de modèles et de paramètres selon les besoins.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"avancé": {
|
||||||
|
"vision": {
|
||||||
|
"model": "llama3.2-vision:90b-instruct-q8_0",
|
||||||
|
"language": "en",
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"model": "mistral",
|
||||||
|
"language": "fr",
|
||||||
|
"temperature": 0.1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Format Markdown exporté
|
||||||
|
|
||||||
|
```md
|
||||||
|
## Figure 3 – Classification des granulats
|
||||||
|
|
||||||
|
**Contexte** :
|
||||||
|
Ce schéma montre...
|
||||||
|
|
||||||
|
**Analyse IA (Vision)** :
|
||||||
|
The diagram shows...
|
||||||
|
|
||||||
|
**Paramètres utilisés** :
|
||||||
|
modèle=llama3.2-vision, temperature=0.2, langue=en
|
||||||
|
|
||||||
|
**Source** :
|
||||||
|
Page 12, Type: Schéma, Fichier: NF P11-300
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Étapes de test recommandées
|
||||||
|
|
||||||
|
1. Charger un PDF
|
||||||
|
2. Sélectionner une zone
|
||||||
|
3. Associer un rôle + agent
|
||||||
|
4. Lancer l’analyse (vision, résumé, etc.)
|
||||||
|
5. Vérifier la sortie
|
||||||
|
6. Exporter le fichier Markdown
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Modes d'analyse et niveaux de charge LLM
|
||||||
|
|
||||||
|
L'utilisateur peut choisir entre **trois niveaux d'analyse** dans l'interface :
|
||||||
|
|
||||||
|
| Niveau | Vision | Résumé / Reformulation | Traduction | Usage recommandé |
|
||||||
|
| --------- | ----------------- | ---------------------- | -------------------- | ------------------------------- |
|
||||||
|
| 🔹 Léger | `llava:34b` | `mistral` | `mistral` | Débogage, prototypes rapides |
|
||||||
|
| ⚪ Moyen | `llava` | `deepseek-r1` | `qwen2.5`, `mistral` | Usage normal |
|
||||||
|
| 🔸 Avancé | `llama3.2-vision` | `deepseek-r1` | `deepseek` | Documents critiques, production |
|
||||||
|
|
||||||
|
### Interface
|
||||||
|
- Menu déroulant "Mode d’analyse" :
|
||||||
|
- Léger / Moyen / Avancé
|
||||||
|
- Ce mode détermine les **modèles, la langue, et les paramètres par défaut**
|
||||||
|
- L'utilisateur peut ensuite **modifier manuellement** chaque agent (surcharger les préréglages)
|
||||||
|
|
||||||
|
### Exemple de workflow
|
||||||
|
1. L'utilisateur sélectionne "Avancé"
|
||||||
|
2. Tous les agents se préconfigurent avec les modèles lourds
|
||||||
|
3. Pour la traduction uniquement, il choisit manuellement `mistral` car il veut aller plus vite
|
||||||
|
4. Le fichier `llm_custom.json` conserve ces réglages personnalisés
|
||||||
|
|
||||||
|
Ce système permet d'adapter la **charge GPU** et la **vitesse de traitement** au besoin sans perdre en contrôle.
|
||||||
49
export_cursor_chats_to_md.py
Normal file
49
export_cursor_chats_to_md.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# === Config ===
|
||||||
|
CURSOR_CHAT_DIR = os.path.expanduser("~/.cursor/chat/")
|
||||||
|
OUTPUT_FILE = "cursor_history.md"
|
||||||
|
|
||||||
|
# === Initialisation du contenu ===
|
||||||
|
md_output = ""
|
||||||
|
|
||||||
|
# === Chargement des discussions Cursor ===
|
||||||
|
for filename in sorted(os.listdir(CURSOR_CHAT_DIR)):
|
||||||
|
if not filename.endswith(".json"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
filepath = os.path.join(CURSOR_CHAT_DIR, filename)
|
||||||
|
|
||||||
|
with open(filepath, "r", encoding="utf-8") as f:
|
||||||
|
try:
|
||||||
|
chat_data = json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
continue # Fichier corrompu ou non lisible
|
||||||
|
|
||||||
|
created_at_raw = chat_data.get("createdAt", "")
|
||||||
|
try:
|
||||||
|
created_at = datetime.fromisoformat(created_at_raw.replace("Z", ""))
|
||||||
|
except ValueError:
|
||||||
|
created_at = datetime.now()
|
||||||
|
|
||||||
|
formatted_time = created_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
md_output += f"\n---\n\n## Session du {formatted_time}\n\n"
|
||||||
|
|
||||||
|
for msg in chat_data.get("messages", []):
|
||||||
|
role = msg.get("role", "")
|
||||||
|
content = msg.get("content", "").strip()
|
||||||
|
if not content:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if role == "user":
|
||||||
|
md_output += f"** Utilisateur :**\n{content}\n\n"
|
||||||
|
elif role == "assistant":
|
||||||
|
md_output += f"** Assistant :**\n{content}\n\n"
|
||||||
|
|
||||||
|
# === Écriture / ajout dans le fichier final ===
|
||||||
|
with open(OUTPUT_FILE, "a", encoding="utf-8") as output_file:
|
||||||
|
output_file.write(md_output)
|
||||||
|
|
||||||
|
print(f" Export terminé ! Discussions ajoutées à : {OUTPUT_FILE}")
|
||||||
26
main.py
Normal file
26
main.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Programme de prétraitement PDF avec agents LLM modulables
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from PyQt6.QtWidgets import QApplication
|
||||||
|
|
||||||
|
from ui.viewer import PDFViewer
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Point d'entrée principal de l'application"""
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setApplicationName("Prétraitement PDF pour Ragflow")
|
||||||
|
|
||||||
|
# Création de la fenêtre principale
|
||||||
|
main_window = PDFViewer()
|
||||||
|
main_window.show()
|
||||||
|
|
||||||
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
0
ragproc/lib64
Normal file
0
ragproc/lib64
Normal file
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
PyQt6>=6.4.0
|
||||||
|
PyMuPDF>=1.21.0
|
||||||
|
numpy>=1.22.0
|
||||||
|
pytesseract>=0.3.9
|
||||||
|
Pillow>=9.3.0
|
||||||
|
opencv-python>=4.7.0
|
||||||
|
requests>=2.28.0
|
||||||
32
setup.py
Normal file
32
setup.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
with open("README.md", "r", encoding="utf-8") as fh:
|
||||||
|
long_description = fh.read()
|
||||||
|
|
||||||
|
with open("requirements.txt", "r", encoding="utf-8") as f:
|
||||||
|
requirements = f.read().splitlines()
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="ragflow_pretraitement",
|
||||||
|
version="1.0.0",
|
||||||
|
author="Ragflow Team",
|
||||||
|
description="Outil de prétraitement PDF avec agents LLM modulables pour Ragflow",
|
||||||
|
long_description=long_description,
|
||||||
|
long_description_content_type="text/markdown",
|
||||||
|
packages=find_packages(),
|
||||||
|
install_requires=requirements,
|
||||||
|
classifiers=[
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"License :: OSI Approved :: MIT License",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
],
|
||||||
|
python_requires=">=3.8",
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"ragflow-pretraitement=ragflow_pretraitement.main:main",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)
|
||||||
8
ui/__init__.py
Normal file
8
ui/__init__.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Module UI
|
||||||
|
from .viewer import PDFViewer
|
||||||
|
from .llm_config_panel import LLMConfigPanel
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'PDFViewer',
|
||||||
|
'LLMConfigPanel'
|
||||||
|
]
|
||||||
BIN
ui/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
ui/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/llm_config_panel.cpython-312.pyc
Normal file
BIN
ui/__pycache__/llm_config_panel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
ui/__pycache__/viewer.cpython-312.pyc
Normal file
BIN
ui/__pycache__/viewer.cpython-312.pyc
Normal file
Binary file not shown.
431
ui/llm_config_panel.py
Normal file
431
ui/llm_config_panel.py
Normal file
@ -0,0 +1,431 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Panneau de configuration des agents LLM
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel,
|
||||||
|
QPushButton, QComboBox, QTextEdit, QGroupBox,
|
||||||
|
QListWidget, QSplitter, QTabWidget, QSpinBox,
|
||||||
|
QDoubleSpinBox, QFormLayout, QMessageBox, QCheckBox)
|
||||||
|
from PyQt6.QtCore import Qt, QSize
|
||||||
|
|
||||||
|
class LLMConfigPanel(QWidget):
|
||||||
|
"""Panneau de configuration des agents LLM et de gestion des sélections"""
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
self.current_selection = None
|
||||||
|
self.analysis_results = {}
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
self.load_llm_profiles()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""Initialise l'interface utilisateur du panneau"""
|
||||||
|
main_layout = QVBoxLayout(self)
|
||||||
|
|
||||||
|
# Tabs pour organiser les fonctionnalités
|
||||||
|
tabs = QTabWidget()
|
||||||
|
main_layout.addWidget(tabs)
|
||||||
|
|
||||||
|
# 1. Onglet Sélections
|
||||||
|
selections_tab = QWidget()
|
||||||
|
tab_layout = QVBoxLayout(selections_tab)
|
||||||
|
|
||||||
|
# Liste des sélections
|
||||||
|
selection_group = QGroupBox("Régions sélectionnées")
|
||||||
|
selection_layout = QVBoxLayout(selection_group)
|
||||||
|
|
||||||
|
self.selection_list = QListWidget()
|
||||||
|
self.selection_list.setMinimumHeight(100)
|
||||||
|
self.selection_list.currentRowChanged.connect(self.selection_changed)
|
||||||
|
selection_layout.addWidget(self.selection_list)
|
||||||
|
|
||||||
|
# Boutons pour la gestion des sélections
|
||||||
|
selection_btns = QHBoxLayout()
|
||||||
|
|
||||||
|
self.remove_btn = QPushButton("Supprimer")
|
||||||
|
self.remove_btn.clicked.connect(self.remove_selection)
|
||||||
|
selection_btns.addWidget(self.remove_btn)
|
||||||
|
|
||||||
|
selection_layout.addLayout(selection_btns)
|
||||||
|
tab_layout.addWidget(selection_group)
|
||||||
|
|
||||||
|
# Type de la sélection
|
||||||
|
type_group = QGroupBox("Type de contenu")
|
||||||
|
type_layout = QVBoxLayout(type_group)
|
||||||
|
|
||||||
|
self.type_combo = QComboBox()
|
||||||
|
self.type_combo.addItems(["schéma", "tableau", "formule", "texte", "autre"])
|
||||||
|
self.type_combo.currentTextChanged.connect(self.update_selection_type)
|
||||||
|
type_layout.addWidget(self.type_combo)
|
||||||
|
|
||||||
|
tab_layout.addWidget(type_group)
|
||||||
|
|
||||||
|
# Contexte textuel
|
||||||
|
context_group = QGroupBox("Contexte textuel")
|
||||||
|
context_layout = QVBoxLayout(context_group)
|
||||||
|
|
||||||
|
self.context_edit = QTextEdit()
|
||||||
|
self.context_edit.setPlaceholderText("Ajoutez ici le contexte pour cette sélection...")
|
||||||
|
self.context_edit.textChanged.connect(self.update_selection_context)
|
||||||
|
context_layout.addWidget(self.context_edit)
|
||||||
|
|
||||||
|
tab_layout.addWidget(context_group)
|
||||||
|
|
||||||
|
# 2. Onglet Agents LLM
|
||||||
|
agent_tab = QWidget()
|
||||||
|
agent_layout = QVBoxLayout(agent_tab)
|
||||||
|
|
||||||
|
# Mode d'analyse
|
||||||
|
mode_group = QGroupBox("Mode d'analyse")
|
||||||
|
mode_layout = QHBoxLayout(mode_group)
|
||||||
|
|
||||||
|
mode_label = QLabel("Niveau :")
|
||||||
|
mode_layout.addWidget(mode_label)
|
||||||
|
|
||||||
|
self.mode_combo = QComboBox()
|
||||||
|
self.mode_combo.addItems(["🔹 Léger", "⚪ Moyen", "🔸 Avancé"])
|
||||||
|
self.mode_combo.currentTextChanged.connect(self.update_mode)
|
||||||
|
mode_layout.addWidget(self.mode_combo)
|
||||||
|
|
||||||
|
agent_layout.addWidget(mode_group)
|
||||||
|
|
||||||
|
# Sélection des agents
|
||||||
|
agent_config_group = QGroupBox("Configuration des agents")
|
||||||
|
agent_config_layout = QVBoxLayout(agent_config_group)
|
||||||
|
|
||||||
|
# Sélection du type d'agent
|
||||||
|
agent_type_layout = QFormLayout()
|
||||||
|
|
||||||
|
# Agent Vision
|
||||||
|
agent_type_layout.addRow(QLabel("<b>Agent Vision</b>"))
|
||||||
|
|
||||||
|
self.vision_model_combo = QComboBox()
|
||||||
|
self.vision_model_combo.addItems(["llava:34b", "llava", "llama3.2-vision"])
|
||||||
|
agent_type_layout.addRow("Modèle:", self.vision_model_combo)
|
||||||
|
|
||||||
|
self.vision_lang_combo = QComboBox()
|
||||||
|
self.vision_lang_combo.addItems(["fr", "en"])
|
||||||
|
agent_type_layout.addRow("Langue:", self.vision_lang_combo)
|
||||||
|
|
||||||
|
# Agent de résumé/reformulation
|
||||||
|
agent_type_layout.addRow(QLabel("<b>Agent Résumé</b>"))
|
||||||
|
|
||||||
|
self.summary_model_combo = QComboBox()
|
||||||
|
self.summary_model_combo.addItems(["mistral", "deepseek-r1"])
|
||||||
|
agent_type_layout.addRow("Modèle:", self.summary_model_combo)
|
||||||
|
|
||||||
|
# Agent de traduction
|
||||||
|
agent_type_layout.addRow(QLabel("<b>Agent Traduction</b>"))
|
||||||
|
|
||||||
|
self.translation_model_combo = QComboBox()
|
||||||
|
self.translation_model_combo.addItems(["mistral", "qwen2.5", "deepseek"])
|
||||||
|
agent_type_layout.addRow("Modèle:", self.translation_model_combo)
|
||||||
|
|
||||||
|
agent_config_layout.addLayout(agent_type_layout)
|
||||||
|
|
||||||
|
# Paramètres communs
|
||||||
|
params_layout = QFormLayout()
|
||||||
|
params_layout.addRow(QLabel("<b>Paramètres de génération</b>"))
|
||||||
|
|
||||||
|
self.temp_spin = QDoubleSpinBox()
|
||||||
|
self.temp_spin.setRange(0.1, 1.0)
|
||||||
|
self.temp_spin.setSingleStep(0.1)
|
||||||
|
self.temp_spin.setValue(0.2)
|
||||||
|
params_layout.addRow("Température:", self.temp_spin)
|
||||||
|
|
||||||
|
self.top_p_spin = QDoubleSpinBox()
|
||||||
|
self.top_p_spin.setRange(0.1, 1.0)
|
||||||
|
self.top_p_spin.setSingleStep(0.05)
|
||||||
|
self.top_p_spin.setValue(0.95)
|
||||||
|
params_layout.addRow("Top-p:", self.top_p_spin)
|
||||||
|
|
||||||
|
self.token_spin = QSpinBox()
|
||||||
|
self.token_spin.setRange(100, 4000)
|
||||||
|
self.token_spin.setSingleStep(100)
|
||||||
|
self.token_spin.setValue(1024)
|
||||||
|
params_layout.addRow("Max tokens:", self.token_spin)
|
||||||
|
|
||||||
|
agent_config_layout.addLayout(params_layout)
|
||||||
|
|
||||||
|
agent_layout.addWidget(agent_config_group)
|
||||||
|
|
||||||
|
# Boutons d'action
|
||||||
|
action_layout = QHBoxLayout()
|
||||||
|
|
||||||
|
self.run_btn = QPushButton("▶ Appliquer l'agent")
|
||||||
|
self.run_btn.setMinimumHeight(40)
|
||||||
|
self.run_btn.clicked.connect(self.run_agent)
|
||||||
|
action_layout.addWidget(self.run_btn)
|
||||||
|
|
||||||
|
agent_layout.addLayout(action_layout)
|
||||||
|
|
||||||
|
# Aperçu des résultats
|
||||||
|
result_group = QGroupBox("Résultat")
|
||||||
|
result_layout = QVBoxLayout(result_group)
|
||||||
|
|
||||||
|
self.result_text = QTextEdit()
|
||||||
|
self.result_text.setReadOnly(True)
|
||||||
|
self.result_text.setMinimumHeight(100)
|
||||||
|
result_layout.addWidget(self.result_text)
|
||||||
|
|
||||||
|
agent_layout.addWidget(result_group)
|
||||||
|
|
||||||
|
# 3. Onglet Export
|
||||||
|
export_tab = QWidget()
|
||||||
|
export_layout = QVBoxLayout(export_tab)
|
||||||
|
|
||||||
|
export_group = QGroupBox("Options d'export")
|
||||||
|
export_group_layout = QVBoxLayout(export_group)
|
||||||
|
|
||||||
|
export_format_layout = QHBoxLayout()
|
||||||
|
export_format_layout.addWidget(QLabel("Format:"))
|
||||||
|
self.export_format_combo = QComboBox()
|
||||||
|
self.export_format_combo.addItems(["Markdown (.md)"])
|
||||||
|
export_format_layout.addWidget(self.export_format_combo)
|
||||||
|
|
||||||
|
export_group_layout.addLayout(export_format_layout)
|
||||||
|
|
||||||
|
self.include_images_check = QCheckBox("Inclure les images")
|
||||||
|
self.include_images_check.setChecked(True)
|
||||||
|
export_group_layout.addWidget(self.include_images_check)
|
||||||
|
|
||||||
|
export_layout.addWidget(export_group)
|
||||||
|
|
||||||
|
export_btn = QPushButton("Exporter")
|
||||||
|
export_btn.setMinimumHeight(40)
|
||||||
|
export_btn.clicked.connect(self.export_results)
|
||||||
|
export_layout.addWidget(export_btn)
|
||||||
|
|
||||||
|
export_layout.addStretch()
|
||||||
|
|
||||||
|
# Ajout des onglets
|
||||||
|
tabs.addTab(selections_tab, "Sélections")
|
||||||
|
tabs.addTab(agent_tab, "Agents LLM")
|
||||||
|
tabs.addTab(export_tab, "Export")
|
||||||
|
|
||||||
|
def load_llm_profiles(self):
|
||||||
|
"""Charge les profils LLM depuis le fichier de configuration"""
|
||||||
|
try:
|
||||||
|
config_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config")
|
||||||
|
profile_path = os.path.join(config_dir, "llm_profiles.json")
|
||||||
|
|
||||||
|
if os.path.exists(profile_path):
|
||||||
|
with open(profile_path, "r", encoding="utf-8") as f:
|
||||||
|
self.profiles = json.load(f)
|
||||||
|
else:
|
||||||
|
# Profil par défaut si le fichier n'existe pas
|
||||||
|
self.profiles = {
|
||||||
|
"léger": {
|
||||||
|
"vision": {"model": "llava:34b", "language": "en", "temperature": 0.2},
|
||||||
|
"translation": {"model": "mistral", "language": "fr", "temperature": 0.1},
|
||||||
|
"summary": {"model": "mistral", "language": "fr", "temperature": 0.2}
|
||||||
|
},
|
||||||
|
"moyen": {
|
||||||
|
"vision": {"model": "llava", "language": "en", "temperature": 0.2},
|
||||||
|
"translation": {"model": "qwen2.5", "language": "fr", "temperature": 0.1},
|
||||||
|
"summary": {"model": "deepseek-r1", "language": "fr", "temperature": 0.2}
|
||||||
|
},
|
||||||
|
"avancé": {
|
||||||
|
"vision": {"model": "llama3.2-vision", "language": "en", "temperature": 0.2},
|
||||||
|
"translation": {"model": "deepseek", "language": "fr", "temperature": 0.1},
|
||||||
|
"summary": {"model": "deepseek-r1", "language": "fr", "temperature": 0.2}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Créer le répertoire de configuration s'il n'existe pas
|
||||||
|
os.makedirs(config_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Sauvegarder le profil par défaut
|
||||||
|
with open(profile_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.profiles, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors du chargement des profils: {str(e)}")
|
||||||
|
|
||||||
|
def update_selection_list(self):
|
||||||
|
"""Met à jour la liste des sélections"""
|
||||||
|
self.selection_list.clear()
|
||||||
|
|
||||||
|
for i, selection in enumerate(self.parent.selected_regions):
|
||||||
|
page = selection["page"] + 1
|
||||||
|
typ = selection["type"]
|
||||||
|
self.selection_list.addItem(f"Page {page} - {typ}")
|
||||||
|
|
||||||
|
def selection_changed(self, index):
|
||||||
|
"""Appelé lorsque la sélection change dans la liste"""
|
||||||
|
if index >= 0 and index < len(self.parent.selected_regions):
|
||||||
|
selection = self.parent.selected_regions[index]
|
||||||
|
self.current_selection = selection
|
||||||
|
|
||||||
|
# Mettre à jour l'interface
|
||||||
|
self.type_combo.setCurrentText(selection["type"])
|
||||||
|
self.context_edit.setText(selection["context"])
|
||||||
|
|
||||||
|
# Afficher les résultats si disponibles
|
||||||
|
if id(selection) in self.analysis_results:
|
||||||
|
self.result_text.setText(self.analysis_results[id(selection)])
|
||||||
|
else:
|
||||||
|
self.result_text.clear()
|
||||||
|
|
||||||
|
def update_selection_type(self, new_type):
|
||||||
|
"""Met à jour le type de la sélection actuelle"""
|
||||||
|
if self.current_selection:
|
||||||
|
self.current_selection["type"] = new_type
|
||||||
|
self.update_selection_list()
|
||||||
|
|
||||||
|
def update_selection_context(self):
|
||||||
|
"""Met à jour le contexte de la sélection actuelle"""
|
||||||
|
if self.current_selection:
|
||||||
|
self.current_selection["context"] = self.context_edit.toPlainText()
|
||||||
|
|
||||||
|
def remove_selection(self):
|
||||||
|
"""Supprime la sélection actuelle"""
|
||||||
|
if self.current_selection:
|
||||||
|
idx = self.parent.selected_regions.index(self.current_selection)
|
||||||
|
self.parent.selected_regions.remove(self.current_selection)
|
||||||
|
|
||||||
|
# Supprimer les résultats associés
|
||||||
|
if id(self.current_selection) in self.analysis_results:
|
||||||
|
del self.analysis_results[id(self.current_selection)]
|
||||||
|
|
||||||
|
self.current_selection = None
|
||||||
|
self.update_selection_list()
|
||||||
|
|
||||||
|
# Sélectionner le prochain élément si disponible
|
||||||
|
if self.selection_list.count() > 0:
|
||||||
|
new_idx = min(idx, self.selection_list.count() - 1)
|
||||||
|
self.selection_list.setCurrentRow(new_idx)
|
||||||
|
else:
|
||||||
|
self.context_edit.clear()
|
||||||
|
self.result_text.clear()
|
||||||
|
|
||||||
|
def update_mode(self, mode_text):
|
||||||
|
"""Met à jour le mode d'analyse en fonction du niveau sélectionné"""
|
||||||
|
if "léger" in mode_text.lower():
|
||||||
|
profile = self.profiles.get("léger", {})
|
||||||
|
elif "moyen" in mode_text.lower():
|
||||||
|
profile = self.profiles.get("moyen", {})
|
||||||
|
elif "avancé" in mode_text.lower():
|
||||||
|
profile = self.profiles.get("avancé", {})
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Mise à jour des combobox avec les paramètres du profil
|
||||||
|
if "vision" in profile:
|
||||||
|
vision = profile["vision"]
|
||||||
|
if "model" in vision and vision["model"] in [self.vision_model_combo.itemText(i) for i in range(self.vision_model_combo.count())]:
|
||||||
|
self.vision_model_combo.setCurrentText(vision["model"])
|
||||||
|
if "language" in vision:
|
||||||
|
self.vision_lang_combo.setCurrentText(vision["language"])
|
||||||
|
if "temperature" in vision:
|
||||||
|
self.temp_spin.setValue(vision["temperature"])
|
||||||
|
|
||||||
|
if "summary" in profile:
|
||||||
|
summary = profile["summary"]
|
||||||
|
if "model" in summary and summary["model"] in [self.summary_model_combo.itemText(i) for i in range(self.summary_model_combo.count())]:
|
||||||
|
self.summary_model_combo.setCurrentText(summary["model"])
|
||||||
|
|
||||||
|
if "translation" in profile:
|
||||||
|
translation = profile["translation"]
|
||||||
|
if "model" in translation and translation["model"] in [self.translation_model_combo.itemText(i) for i in range(self.translation_model_combo.count())]:
|
||||||
|
self.translation_model_combo.setCurrentText(translation["model"])
|
||||||
|
|
||||||
|
def run_agent(self):
|
||||||
|
"""Execute l'agent LLM sur la sélection actuelle"""
|
||||||
|
if not self.current_selection:
|
||||||
|
QMessageBox.warning(self, "Aucune sélection",
|
||||||
|
"Veuillez sélectionner une région à analyser.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Pour cette démonstration, nous simulons le résultat
|
||||||
|
# Dans une implémentation réelle, nous utiliserions les agents LLM
|
||||||
|
|
||||||
|
# Récupérer les paramètres
|
||||||
|
vision_model = self.vision_model_combo.currentText()
|
||||||
|
summary_model = self.summary_model_combo.currentText()
|
||||||
|
translation_model = self.translation_model_combo.currentText()
|
||||||
|
lang = self.vision_lang_combo.currentText()
|
||||||
|
temp = self.temp_spin.value()
|
||||||
|
|
||||||
|
# Exemple de résultat simulé
|
||||||
|
result = f"**Analyse de l'agent Vision ({vision_model})**\n\n"
|
||||||
|
|
||||||
|
if self.current_selection["type"] == "schéma":
|
||||||
|
result += "Le schéma illustre un processus en plusieurs étapes avec des connections entre les différents éléments.\n\n"
|
||||||
|
elif self.current_selection["type"] == "tableau":
|
||||||
|
result += "Le tableau contient des données structurées avec plusieurs colonnes et rangées.\n\n"
|
||||||
|
elif self.current_selection["type"] == "formule":
|
||||||
|
result += "La formule mathématique représente une équation complexe.\n\n"
|
||||||
|
else:
|
||||||
|
result += "Le contenu sélectionné a été analysé.\n\n"
|
||||||
|
|
||||||
|
result += f"**Résumé ({summary_model})**\n\n"
|
||||||
|
result += "Ce contenu montre l'importance des éléments sélectionnés dans le contexte du document.\n\n"
|
||||||
|
|
||||||
|
if lang == "en":
|
||||||
|
result += f"**Traduction ({translation_model})**\n\n"
|
||||||
|
result += "The selected content has been analyzed and shows the importance of the selected elements in the context of the document.\n\n"
|
||||||
|
|
||||||
|
result += "**Paramètres utilisés**\n"
|
||||||
|
result += f"modèle vision={vision_model}, modèle résumé={summary_model}, "
|
||||||
|
result += f"temperature={temp}, langue={lang}"
|
||||||
|
|
||||||
|
# Stocker et afficher le résultat
|
||||||
|
self.analysis_results[id(self.current_selection)] = result
|
||||||
|
self.result_text.setText(result)
|
||||||
|
|
||||||
|
# Mise à jour du statut
|
||||||
|
self.parent.status_bar.showMessage("Analyse terminée.")
|
||||||
|
|
||||||
|
def export_results(self):
|
||||||
|
"""Exporte les résultats au format Markdown"""
|
||||||
|
if not self.parent.selected_regions:
|
||||||
|
QMessageBox.warning(self, "Aucune sélection",
|
||||||
|
"Il n'y a aucune sélection à exporter.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Créer le répertoire de sortie s'il n'existe pas
|
||||||
|
output_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "outputs")
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Générer le contenu Markdown
|
||||||
|
md_content = "# Analyse de document\n\n"
|
||||||
|
|
||||||
|
# Trier les sélections par numéro de page
|
||||||
|
sorted_selections = sorted(self.parent.selected_regions, key=lambda x: x["page"])
|
||||||
|
|
||||||
|
for selection in sorted_selections:
|
||||||
|
page = selection["page"] + 1
|
||||||
|
typ = selection["type"]
|
||||||
|
context = selection["context"]
|
||||||
|
|
||||||
|
md_content += f"## {typ.capitalize()} - Page {page}\n\n"
|
||||||
|
|
||||||
|
if context:
|
||||||
|
md_content += f"**Contexte** :\n{context}\n\n"
|
||||||
|
|
||||||
|
# Ajouter les résultats d'analyse si disponibles
|
||||||
|
if id(selection) in self.analysis_results:
|
||||||
|
md_content += f"**Analyse IA** :\n{self.analysis_results[id(selection)]}\n\n"
|
||||||
|
|
||||||
|
md_content += "---\n\n"
|
||||||
|
|
||||||
|
# Écrire le fichier Markdown
|
||||||
|
file_path = os.path.join(output_dir, "analyse_document.md")
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(md_content)
|
||||||
|
|
||||||
|
QMessageBox.information(self, "Export réussi",
|
||||||
|
f"Le fichier a été exporté avec succès :\n{file_path}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Erreur d'export",
|
||||||
|
f"Une erreur est survenue lors de l'export :\n{str(e)}")
|
||||||
315
ui/viewer.py
Normal file
315
ui/viewer.py
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Interface de visualisation et de sélection dans les documents PDF
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from PyQt6.QtWidgets import (QMainWindow, QFileDialog, QVBoxLayout, QHBoxLayout,
|
||||||
|
QLabel, QPushButton, QComboBox, QWidget, QScrollArea,
|
||||||
|
QSpinBox, QFrame, QSplitter, QGroupBox, QStatusBar)
|
||||||
|
from PyQt6.QtCore import Qt, QRectF, QPoint, pyqtSignal
|
||||||
|
from PyQt6.QtGui import QPixmap, QPainter, QPen, QColor, QImage
|
||||||
|
|
||||||
|
from .llm_config_panel import LLMConfigPanel
|
||||||
|
import fitz # PyMuPDF
|
||||||
|
|
||||||
|
class PDFViewer(QMainWindow):
|
||||||
|
"""Interface principale pour la visualisation et l'annotation de PDFs"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.pdf_document = None
|
||||||
|
self.current_page = 0
|
||||||
|
self.total_pages = 0
|
||||||
|
self.zoom_factor = 1.0
|
||||||
|
self.selection_rect = None
|
||||||
|
self.selection_start = None
|
||||||
|
self.selection_active = False
|
||||||
|
self.selected_regions = [] # Liste des zones sélectionnées
|
||||||
|
|
||||||
|
self.init_ui()
|
||||||
|
|
||||||
|
def init_ui(self):
|
||||||
|
"""Initialise l'interface utilisateur"""
|
||||||
|
self.setWindowTitle("Prétraitement PDF pour Ragflow")
|
||||||
|
self.setGeometry(100, 100, 1200, 800)
|
||||||
|
|
||||||
|
# Barre d'état
|
||||||
|
self.status_bar = QStatusBar()
|
||||||
|
self.setStatusBar(self.status_bar)
|
||||||
|
self.status_bar.showMessage("Prêt")
|
||||||
|
|
||||||
|
# Widget principal et layout
|
||||||
|
main_widget = QWidget()
|
||||||
|
self.setCentralWidget(main_widget)
|
||||||
|
main_layout = QHBoxLayout(main_widget)
|
||||||
|
|
||||||
|
# Splitter pour séparer la visualisation PDF et le panneau de configuration
|
||||||
|
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||||
|
main_layout.addWidget(splitter)
|
||||||
|
|
||||||
|
# =================== Partie gauche: Visualisation PDF ====================
|
||||||
|
pdf_panel = QWidget()
|
||||||
|
pdf_layout = QVBoxLayout(pdf_panel)
|
||||||
|
|
||||||
|
# Barre d'outils pour le PDF
|
||||||
|
toolbar = QWidget()
|
||||||
|
toolbar_layout = QHBoxLayout(toolbar)
|
||||||
|
toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
|
# Bouton pour charger un PDF
|
||||||
|
self.load_btn = QPushButton("Charger PDF")
|
||||||
|
self.load_btn.clicked.connect(self.load_pdf)
|
||||||
|
toolbar_layout.addWidget(self.load_btn)
|
||||||
|
|
||||||
|
# Navigation
|
||||||
|
self.prev_btn = QPushButton("Page précédente")
|
||||||
|
self.prev_btn.clicked.connect(self.prev_page)
|
||||||
|
self.prev_btn.setEnabled(False)
|
||||||
|
toolbar_layout.addWidget(self.prev_btn)
|
||||||
|
|
||||||
|
self.page_spin = QSpinBox()
|
||||||
|
self.page_spin.setMinimum(1)
|
||||||
|
self.page_spin.setMaximum(1)
|
||||||
|
self.page_spin.valueChanged.connect(self.go_to_page)
|
||||||
|
toolbar_layout.addWidget(self.page_spin)
|
||||||
|
|
||||||
|
self.page_count_label = QLabel(" / 1")
|
||||||
|
toolbar_layout.addWidget(self.page_count_label)
|
||||||
|
|
||||||
|
self.next_btn = QPushButton("Page suivante")
|
||||||
|
self.next_btn.clicked.connect(self.next_page)
|
||||||
|
self.next_btn.setEnabled(False)
|
||||||
|
toolbar_layout.addWidget(self.next_btn)
|
||||||
|
|
||||||
|
# Zoom
|
||||||
|
self.zoom_in_btn = QPushButton("Zoom +")
|
||||||
|
self.zoom_in_btn.clicked.connect(self.zoom_in)
|
||||||
|
toolbar_layout.addWidget(self.zoom_in_btn)
|
||||||
|
|
||||||
|
self.zoom_out_btn = QPushButton("Zoom -")
|
||||||
|
self.zoom_out_btn.clicked.connect(self.zoom_out)
|
||||||
|
toolbar_layout.addWidget(self.zoom_out_btn)
|
||||||
|
|
||||||
|
# Bouton pour effacer la sélection
|
||||||
|
self.clear_sel_btn = QPushButton("Effacer sélection")
|
||||||
|
self.clear_sel_btn.clicked.connect(self.clear_selection)
|
||||||
|
toolbar_layout.addWidget(self.clear_sel_btn)
|
||||||
|
|
||||||
|
pdf_layout.addWidget(toolbar)
|
||||||
|
|
||||||
|
# Zone de visualisation du PDF
|
||||||
|
self.scroll_area = QScrollArea()
|
||||||
|
self.scroll_area.setWidgetResizable(True)
|
||||||
|
self.scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
self.pdf_label = PDFLabel(self)
|
||||||
|
self.scroll_area.setWidget(self.pdf_label)
|
||||||
|
|
||||||
|
pdf_layout.addWidget(self.scroll_area)
|
||||||
|
|
||||||
|
# =================== Partie droite: Panel de configuration ====================
|
||||||
|
self.config_panel = LLMConfigPanel(self)
|
||||||
|
|
||||||
|
# Ajout des deux panneaux au splitter
|
||||||
|
splitter.addWidget(pdf_panel)
|
||||||
|
splitter.addWidget(self.config_panel)
|
||||||
|
|
||||||
|
# Définir les proportions de départ du splitter
|
||||||
|
splitter.setSizes([700, 500])
|
||||||
|
|
||||||
|
def load_pdf(self):
|
||||||
|
"""Ouvre un dialogue pour charger un fichier PDF"""
|
||||||
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "Ouvrir un fichier PDF", "", "Fichiers PDF (*.pdf)"
|
||||||
|
)
|
||||||
|
|
||||||
|
if file_path:
|
||||||
|
try:
|
||||||
|
self.pdf_document = fitz.open(file_path)
|
||||||
|
self.total_pages = len(self.pdf_document)
|
||||||
|
self.current_page = 0
|
||||||
|
|
||||||
|
# Mise à jour de l'interface
|
||||||
|
self.page_spin.setMaximum(self.total_pages)
|
||||||
|
self.page_count_label.setText(f" / {self.total_pages}")
|
||||||
|
self.page_spin.setValue(1) # Cette action déclenche go_to_page()
|
||||||
|
|
||||||
|
self.prev_btn.setEnabled(True)
|
||||||
|
self.next_btn.setEnabled(True)
|
||||||
|
|
||||||
|
self.render_current_page()
|
||||||
|
|
||||||
|
# Mise à jour du titre avec le nom du fichier
|
||||||
|
file_name = os.path.basename(file_path)
|
||||||
|
self.setWindowTitle(f"Prétraitement PDF - {file_name}")
|
||||||
|
self.status_bar.showMessage(f"PDF chargé: {file_name}, {self.total_pages} pages")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.status_bar.showMessage(f"Erreur lors du chargement du PDF: {str(e)}")
|
||||||
|
|
||||||
|
def render_current_page(self):
|
||||||
|
"""Affiche la page courante du PDF"""
|
||||||
|
if not self.pdf_document:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Récupérer la page actuelle
|
||||||
|
page = self.pdf_document[self.current_page]
|
||||||
|
|
||||||
|
# Facteur de zoom et de qualité pour le rendu
|
||||||
|
zoom_matrix = fitz.Matrix(2 * self.zoom_factor, 2 * self.zoom_factor)
|
||||||
|
|
||||||
|
# Rendu de la page en un pixmap
|
||||||
|
pixmap = page.get_pixmap(matrix=zoom_matrix, alpha=False)
|
||||||
|
|
||||||
|
# Conversion en QImage puis QPixmap
|
||||||
|
img = QImage(pixmap.samples, pixmap.width, pixmap.height,
|
||||||
|
pixmap.stride, QImage.Format.Format_RGB888)
|
||||||
|
|
||||||
|
pixmap_qt = QPixmap.fromImage(img)
|
||||||
|
|
||||||
|
# Mise à jour de l'affichage
|
||||||
|
self.pdf_label.setPixmap(pixmap_qt)
|
||||||
|
self.pdf_label.adjustSize()
|
||||||
|
|
||||||
|
# Mise à jour de l'état
|
||||||
|
self.status_bar.showMessage(f"Page {self.current_page + 1}/{self.total_pages}")
|
||||||
|
|
||||||
|
def next_page(self):
|
||||||
|
"""Passe à la page suivante"""
|
||||||
|
if self.pdf_document and self.current_page < self.total_pages - 1:
|
||||||
|
self.current_page += 1
|
||||||
|
self.page_spin.setValue(self.current_page + 1)
|
||||||
|
|
||||||
|
def prev_page(self):
|
||||||
|
"""Passe à la page précédente"""
|
||||||
|
if self.pdf_document and self.current_page > 0:
|
||||||
|
self.current_page -= 1
|
||||||
|
self.page_spin.setValue(self.current_page + 1)
|
||||||
|
|
||||||
|
def go_to_page(self, page_num):
|
||||||
|
"""Va à une page spécifique"""
|
||||||
|
if self.pdf_document and 1 <= page_num <= self.total_pages:
|
||||||
|
self.current_page = page_num - 1
|
||||||
|
self.render_current_page()
|
||||||
|
|
||||||
|
def zoom_in(self):
|
||||||
|
"""Augmente le facteur de zoom"""
|
||||||
|
self.zoom_factor *= 1.25
|
||||||
|
self.render_current_page()
|
||||||
|
|
||||||
|
def zoom_out(self):
|
||||||
|
"""Diminue le facteur de zoom"""
|
||||||
|
self.zoom_factor *= 0.8
|
||||||
|
self.render_current_page()
|
||||||
|
|
||||||
|
def clear_selection(self):
|
||||||
|
"""Efface la sélection actuelle"""
|
||||||
|
self.selection_rect = None
|
||||||
|
self.selection_active = False
|
||||||
|
self.selection_start = None
|
||||||
|
self.pdf_label.update()
|
||||||
|
|
||||||
|
def add_selection(self, rect, page_num=None):
|
||||||
|
"""
|
||||||
|
Ajoute une sélection à la liste des régions sélectionnées
|
||||||
|
|
||||||
|
Args:
|
||||||
|
rect (QRectF): Rectangle de sélection
|
||||||
|
page_num (int, optional): Numéro de page. Si None, utilise la page courante.
|
||||||
|
"""
|
||||||
|
page = self.current_page if page_num is None else page_num
|
||||||
|
|
||||||
|
# Ajuster les coordonnées selon le zoom actuel
|
||||||
|
adjusted_rect = QRectF(
|
||||||
|
rect.x() / self.zoom_factor,
|
||||||
|
rect.y() / self.zoom_factor,
|
||||||
|
rect.width() / self.zoom_factor,
|
||||||
|
rect.height() / self.zoom_factor
|
||||||
|
)
|
||||||
|
|
||||||
|
selection = {
|
||||||
|
"page": page,
|
||||||
|
"rect": adjusted_rect,
|
||||||
|
"type": "schéma", # Type par défaut
|
||||||
|
"context": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
self.selected_regions.append(selection)
|
||||||
|
self.config_panel.update_selection_list()
|
||||||
|
|
||||||
|
# Informer l'utilisateur
|
||||||
|
self.status_bar.showMessage(f"Sélection ajoutée à la page {page + 1}")
|
||||||
|
|
||||||
|
|
||||||
|
class PDFLabel(QLabel):
|
||||||
|
"""Étiquette personnalisée pour afficher le PDF et gérer les sélections"""
|
||||||
|
|
||||||
|
selection_made = pyqtSignal(QRectF)
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.parent = parent
|
||||||
|
self.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
|
|
||||||
|
# Pour permettre de suivre les événements de souris
|
||||||
|
self.setMouseTracking(True)
|
||||||
|
|
||||||
|
def mousePressEvent(self, event):
|
||||||
|
"""Gère l'événement de clic de souris pour démarrer une sélection"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton:
|
||||||
|
self.parent.selection_start = event.position()
|
||||||
|
self.parent.selection_active = True
|
||||||
|
self.parent.selection_rect = QRectF(event.position(), event.position())
|
||||||
|
|
||||||
|
def mouseMoveEvent(self, event):
|
||||||
|
"""Gère l'événement de déplacement de souris pour mettre à jour la sélection"""
|
||||||
|
if self.parent.selection_active:
|
||||||
|
# Mise à jour du rectangle de sélection
|
||||||
|
self.parent.selection_rect = QRectF(
|
||||||
|
self.parent.selection_start,
|
||||||
|
event.position()
|
||||||
|
).normalized()
|
||||||
|
# Demande de mise à jour de l'affichage
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, event):
|
||||||
|
"""Gère l'événement de relâchement de souris pour finaliser une sélection"""
|
||||||
|
if event.button() == Qt.MouseButton.LeftButton and self.parent.selection_active:
|
||||||
|
# Finalisation de la sélection
|
||||||
|
self.parent.selection_active = False
|
||||||
|
|
||||||
|
# Vérifier que la sélection a une taille minimale
|
||||||
|
if (self.parent.selection_rect.width() > 10 and
|
||||||
|
self.parent.selection_rect.height() > 10):
|
||||||
|
|
||||||
|
# Ajouter cette sélection à la liste des sélections
|
||||||
|
self.parent.add_selection(self.parent.selection_rect)
|
||||||
|
|
||||||
|
# Émettre le signal de sélection
|
||||||
|
self.selection_made.emit(self.parent.selection_rect)
|
||||||
|
|
||||||
|
# Continuer à afficher le rectangle
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
"""Surcharge pour dessiner la sélection par-dessus le PDF"""
|
||||||
|
super().paintEvent(event)
|
||||||
|
|
||||||
|
# Dessiner le rectangle de sélection si disponible
|
||||||
|
if self.parent.selection_rect is not None:
|
||||||
|
painter = QPainter(self)
|
||||||
|
|
||||||
|
# Paramètres pour le rectangle de sélection
|
||||||
|
pen = QPen(QColor(255, 0, 0)) # Rouge
|
||||||
|
pen.setWidth(2)
|
||||||
|
pen.setStyle(Qt.PenStyle.DashLine)
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
# Dessiner avec une légère transparence
|
||||||
|
painter.setOpacity(0.7)
|
||||||
|
painter.drawRect(self.parent.selection_rect)
|
||||||
10
utils/__init__.py
Normal file
10
utils/__init__.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Module Utils
|
||||||
|
from .markdown_export import MarkdownExporter
|
||||||
|
from .api_ollama import OllamaAPI
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .ocr import OCRProcessor
|
||||||
|
__all__ = ['MarkdownExporter', 'OllamaAPI', 'OCRProcessor']
|
||||||
|
except ImportError:
|
||||||
|
# OCR facultatif car il dépend de bibliothèques externes supplémentaires
|
||||||
|
__all__ = ['MarkdownExporter', 'OllamaAPI']
|
||||||
244
utils/api_ollama.py
Normal file
244
utils/api_ollama.py
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Module pour l'interaction avec l'API Ollama
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
from typing import List, Dict, Any, Optional, Union, Callable
|
||||||
|
|
||||||
|
|
||||||
|
class OllamaAPI:
|
||||||
|
"""
|
||||||
|
Classe pour interagir avec l'API Ollama
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str = "http://217.182.105.173:11434"):
|
||||||
|
"""
|
||||||
|
Initialise la connexion à l'API Ollama
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url (str): URL de base de l'API Ollama
|
||||||
|
"""
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.generate_endpoint = f"{self.base_url}/api/generate"
|
||||||
|
self.chat_endpoint = f"{self.base_url}/api/chat"
|
||||||
|
self.models_endpoint = f"{self.base_url}/api/tags"
|
||||||
|
|
||||||
|
def list_models(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Récupère la liste des modèles disponibles
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[str]: Liste des noms de modèles disponibles
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(self.models_endpoint)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Extraire les noms des modèles
|
||||||
|
models = [model['name'] for model in data.get('models', [])]
|
||||||
|
return models
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de la récupération des modèles: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def generate(self, model: str, prompt: str, images: Optional[List[bytes]] = None,
|
||||||
|
options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Génère une réponse à partir d'un prompt
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model (str): Nom du modèle à utiliser
|
||||||
|
prompt (str): Texte du prompt
|
||||||
|
images (List[bytes], optional): Liste d'images en bytes
|
||||||
|
options (Dict, optional): Options de génération
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Réponse du modèle
|
||||||
|
"""
|
||||||
|
# Options par défaut
|
||||||
|
default_options = {
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"num_predict": 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fusionner avec les options fournies
|
||||||
|
if options:
|
||||||
|
default_options.update(options)
|
||||||
|
|
||||||
|
# Construire la payload
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"options": default_options
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajouter les images si fournies (pour les modèles multimodaux)
|
||||||
|
if images:
|
||||||
|
base64_images = []
|
||||||
|
for img in images:
|
||||||
|
if isinstance(img, bytes):
|
||||||
|
base64_img = base64.b64encode(img).decode("utf-8")
|
||||||
|
base64_images.append(base64_img)
|
||||||
|
|
||||||
|
payload["images"] = base64_images
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Envoyer la requête
|
||||||
|
response = requests.post(self.generate_endpoint, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
print(f"Erreur HTTP: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de la génération: {str(e)}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def chat(self, model: str, messages: List[Dict[str, Any]],
|
||||||
|
images: Optional[List[bytes]] = None,
|
||||||
|
options: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Utilise l'API de chat pour une conversation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model (str): Nom du modèle à utiliser
|
||||||
|
messages (List[Dict]): Liste des messages de la conversation
|
||||||
|
Format: [{"role": "user", "content": "message"}, ...]
|
||||||
|
images (List[bytes], optional): Liste d'images en bytes (pour le dernier message)
|
||||||
|
options (Dict, optional): Options de génération
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Réponse du modèle
|
||||||
|
"""
|
||||||
|
# Options par défaut
|
||||||
|
default_options = {
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"num_predict": 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fusionner avec les options fournies
|
||||||
|
if options:
|
||||||
|
default_options.update(options)
|
||||||
|
|
||||||
|
# Construire la payload
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"messages": messages,
|
||||||
|
"options": default_options
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ajouter les images au dernier message utilisateur si fournies
|
||||||
|
if images and messages and messages[-1]["role"] == "user":
|
||||||
|
base64_images = []
|
||||||
|
for img in images:
|
||||||
|
if isinstance(img, bytes):
|
||||||
|
base64_img = base64.b64encode(img).decode("utf-8")
|
||||||
|
base64_images.append(base64_img)
|
||||||
|
|
||||||
|
# Modifier le dernier message pour inclure les images
|
||||||
|
last_message = messages[-1].copy()
|
||||||
|
# Les images doivent être dans un champ distinct du modèle d'API d'Ollama
|
||||||
|
# Pas comme un champ texte standard mais dans un tableau d'images
|
||||||
|
if "images" not in last_message:
|
||||||
|
last_message["images"] = base64_images
|
||||||
|
|
||||||
|
# Remplacer le dernier message
|
||||||
|
payload["messages"] = messages[:-1] + [last_message]
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Envoyer la requête
|
||||||
|
response = requests.post(self.chat_endpoint, json=payload)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
print(f"Erreur HTTP: {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors du chat: {str(e)}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def stream_generate(self, model: str, prompt: str,
|
||||||
|
callback: Callable[[str], None],
|
||||||
|
options: Optional[Dict[str, Any]] = None) -> None:
|
||||||
|
"""
|
||||||
|
Génère une réponse en streaming et appelle le callback pour chaque morceau
|
||||||
|
|
||||||
|
Args:
|
||||||
|
model (str): Nom du modèle à utiliser
|
||||||
|
prompt (str): Texte du prompt
|
||||||
|
callback (Callable): Fonction à appeler pour chaque morceau de texte
|
||||||
|
options (Dict, optional): Options de génération
|
||||||
|
"""
|
||||||
|
# Options par défaut
|
||||||
|
default_options = {
|
||||||
|
"temperature": 0.2,
|
||||||
|
"top_p": 0.95,
|
||||||
|
"top_k": 40,
|
||||||
|
"num_predict": 1024,
|
||||||
|
"stream": True # Activer le streaming
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fusionner avec les options fournies
|
||||||
|
if options:
|
||||||
|
default_options.update(options)
|
||||||
|
|
||||||
|
# S'assurer que stream est activé
|
||||||
|
default_options["stream"] = True
|
||||||
|
|
||||||
|
# Construire la payload
|
||||||
|
payload = {
|
||||||
|
"model": model,
|
||||||
|
"prompt": prompt,
|
||||||
|
"options": default_options
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Envoyer la requête en streaming
|
||||||
|
with requests.post(self.generate_endpoint, json=payload, stream=True) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
# Traiter chaque ligne de la réponse
|
||||||
|
for line in response.iter_lines():
|
||||||
|
if line:
|
||||||
|
try:
|
||||||
|
data = json.loads(line)
|
||||||
|
if "response" in data:
|
||||||
|
callback(data["response"])
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Erreur de décodage JSON: {line}")
|
||||||
|
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
print(f"Erreur HTTP: {e}")
|
||||||
|
callback(f"\nErreur: {str(e)}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors du streaming: {str(e)}")
|
||||||
|
callback(f"\nErreur: {str(e)}")
|
||||||
|
|
||||||
|
def check_connection(self) -> bool:
|
||||||
|
"""
|
||||||
|
Vérifie si la connexion à l'API Ollama est fonctionnelle
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True si la connexion est établie, False sinon
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{self.base_url}/api/version")
|
||||||
|
return response.status_code == 200
|
||||||
|
except:
|
||||||
|
return False
|
||||||
224
utils/markdown_export.py
Normal file
224
utils/markdown_export.py
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Module pour l'exportation des résultats au format Markdown
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import base64
|
||||||
|
import re
|
||||||
|
from typing import List, Dict, Any, Optional, Union
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownExporter:
|
||||||
|
"""
|
||||||
|
Classe pour exporter les résultats d'analyse en Markdown
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, output_dir: Optional[str] = "") -> None:
|
||||||
|
"""
|
||||||
|
Initialise l'exporteur Markdown
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_dir (str, optional): Répertoire de sortie pour les fichiers générés
|
||||||
|
"""
|
||||||
|
if not output_dir:
|
||||||
|
self.output_dir = os.path.join(os.getcwd(), "data", "outputs")
|
||||||
|
else:
|
||||||
|
self.output_dir = output_dir
|
||||||
|
|
||||||
|
os.makedirs(self.output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Compteur pour les images exportées
|
||||||
|
self.image_counter = 0
|
||||||
|
|
||||||
|
def create_markdown_from_selections(self, selections: List[Dict[str, Any]],
|
||||||
|
analysis_results: Dict[int, str],
|
||||||
|
document_title: str = "Analyse de document",
|
||||||
|
include_images: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
Crée un document Markdown à partir des sélections et des résultats d'analyse
|
||||||
|
|
||||||
|
Args:
|
||||||
|
selections (List[Dict]): Liste des sélections (régions d'intérêt)
|
||||||
|
analysis_results (Dict): Dictionnaire des résultats d'analyse par ID de sélection
|
||||||
|
document_title (str): Titre du document Markdown
|
||||||
|
include_images (bool): Si True, inclut les images dans le Markdown
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Contenu du document Markdown généré
|
||||||
|
"""
|
||||||
|
# Trier les sélections par numéro de page
|
||||||
|
sorted_selections = sorted(selections, key=lambda x: x.get("page", 0))
|
||||||
|
|
||||||
|
# Commencer le document avec le titre
|
||||||
|
markdown_content = f"# {document_title}\n\n"
|
||||||
|
|
||||||
|
# Ajouter chaque sélection
|
||||||
|
for selection in sorted_selections:
|
||||||
|
page = selection.get("page", 0) + 1
|
||||||
|
selection_type = selection.get("type", "zone")
|
||||||
|
context = selection.get("context", "")
|
||||||
|
|
||||||
|
# Titre de la section
|
||||||
|
markdown_content += f"## {selection_type.capitalize()} - Page {page}\n\n"
|
||||||
|
|
||||||
|
# Ajouter l'image si demandé et disponible
|
||||||
|
if include_images and "image_data" in selection:
|
||||||
|
image_path = self._save_image(selection["image_data"])
|
||||||
|
if image_path:
|
||||||
|
rel_path = os.path.relpath(image_path, self.output_dir)
|
||||||
|
markdown_content += f"\n\n"
|
||||||
|
|
||||||
|
# Ajouter le contexte
|
||||||
|
if context:
|
||||||
|
markdown_content += f"**Contexte** :\n{context}\n\n"
|
||||||
|
|
||||||
|
# Ajouter les résultats d'analyse
|
||||||
|
selection_id = id(selection)
|
||||||
|
if selection_id in analysis_results:
|
||||||
|
markdown_content += f"**Analyse IA** :\n{analysis_results[selection_id]}\n\n"
|
||||||
|
|
||||||
|
# Ajouter un séparateur
|
||||||
|
markdown_content += "---\n\n"
|
||||||
|
|
||||||
|
return markdown_content
|
||||||
|
|
||||||
|
def export_to_file(self, markdown_content: str, filename: str = "analyse_document.md") -> str:
|
||||||
|
"""
|
||||||
|
Exporte le contenu Markdown dans un fichier
|
||||||
|
|
||||||
|
Args:
|
||||||
|
markdown_content (str): Contenu Markdown à exporter
|
||||||
|
filename (str): Nom du fichier de sortie
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Chemin absolu du fichier créé
|
||||||
|
"""
|
||||||
|
file_path = os.path.join(self.output_dir, filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(markdown_content)
|
||||||
|
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de l'exportation du fichier Markdown: {str(e)}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _save_image(self, image_data: bytes, prefix: str = "image") -> Optional[str]:
|
||||||
|
"""
|
||||||
|
Sauvegarde une image et retourne son chemin
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image_data (bytes): Données de l'image
|
||||||
|
prefix (str): Préfixe pour le nom du fichier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: Chemin de l'image sauvegardée ou None en cas d'erreur
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Incrémenter le compteur d'images
|
||||||
|
self.image_counter += 1
|
||||||
|
|
||||||
|
# Créer le répertoire d'images s'il n'existe pas
|
||||||
|
images_dir = os.path.join(self.output_dir, "images")
|
||||||
|
os.makedirs(images_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Chemin de l'image
|
||||||
|
image_path = os.path.join(images_dir, f"{prefix}_{self.image_counter}.png")
|
||||||
|
|
||||||
|
# Sauvegarder l'image
|
||||||
|
with open(image_path, "wb") as f:
|
||||||
|
f.write(image_data)
|
||||||
|
|
||||||
|
return image_path
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de la sauvegarde de l'image: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def format_analysis_result(self, result: str, include_parameters: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
Formate un résultat d'analyse pour le Markdown
|
||||||
|
|
||||||
|
Args:
|
||||||
|
result (str): Texte du résultat d'analyse
|
||||||
|
include_parameters (bool): Si True, inclut la section des paramètres
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Résultat formaté pour le Markdown
|
||||||
|
"""
|
||||||
|
# Simple nettoyage des sections
|
||||||
|
formatted = result
|
||||||
|
|
||||||
|
# Séparer les paramètres
|
||||||
|
if not include_parameters and "**Paramètres utilisés**" in result:
|
||||||
|
formatted = result.split("**Paramètres utilisés**")[0].strip()
|
||||||
|
|
||||||
|
return formatted
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def sanitize_filename(title: str) -> str:
|
||||||
|
"""
|
||||||
|
Convertit un titre en nom de fichier sécurisé
|
||||||
|
|
||||||
|
Args:
|
||||||
|
title (str): Titre à convertir
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Nom de fichier sécurisé
|
||||||
|
"""
|
||||||
|
# Remplacer les caractères non alphanumériques par des tirets
|
||||||
|
sanitized = re.sub(r'[^a-zA-Z0-9]', '-', title.lower())
|
||||||
|
# Réduire les tirets multiples à un seul
|
||||||
|
sanitized = re.sub(r'-+', '-', sanitized)
|
||||||
|
# Limiter la longueur et supprimer les tirets au début/fin
|
||||||
|
return sanitized[:50].strip('-')
|
||||||
|
|
||||||
|
def embed_images_in_markdown(self, markdown_content: str) -> str:
|
||||||
|
"""
|
||||||
|
Remplace les références d'images par des images en base64 intégrées
|
||||||
|
|
||||||
|
Args:
|
||||||
|
markdown_content (str): Contenu Markdown avec références d'images
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Contenu Markdown avec images intégrées
|
||||||
|
"""
|
||||||
|
# Regex pour trouver les références d'images
|
||||||
|
img_pattern = r'!\[(.*?)\]\((.*?)\)'
|
||||||
|
|
||||||
|
def replace_image(match):
|
||||||
|
alt_text = match.group(1)
|
||||||
|
img_path = match.group(2)
|
||||||
|
|
||||||
|
# Résoudre le chemin relatif
|
||||||
|
if not os.path.isabs(img_path):
|
||||||
|
img_path = os.path.join(self.output_dir, img_path)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Lire l'image et l'encoder en base64
|
||||||
|
with open(img_path, "rb") as img_file:
|
||||||
|
img_data = img_file.read()
|
||||||
|
img_base64 = base64.b64encode(img_data).decode('utf-8')
|
||||||
|
|
||||||
|
# Déterminer le type MIME
|
||||||
|
if img_path.lower().endswith('.png'):
|
||||||
|
mime_type = 'image/png'
|
||||||
|
elif img_path.lower().endswith(('.jpg', '.jpeg')):
|
||||||
|
mime_type = 'image/jpeg'
|
||||||
|
else:
|
||||||
|
mime_type = 'image/png' # par défaut
|
||||||
|
|
||||||
|
# Retourner l'image intégrée en base64
|
||||||
|
return f''
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de l'intégration de l'image {img_path}: {str(e)}")
|
||||||
|
return match.group(0) # Conserver l'original en cas d'erreur
|
||||||
|
|
||||||
|
# Remplacer toutes les références d'images
|
||||||
|
return re.sub(img_pattern, replace_image, markdown_content)
|
||||||
259
utils/ocr.py
Normal file
259
utils/ocr.py
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
"""
|
||||||
|
Module OCR pour la reconnaissance de texte dans les images
|
||||||
|
"""
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
import pytesseract
|
||||||
|
from typing import Union, Dict, Tuple, Optional
|
||||||
|
from PIL import Image
|
||||||
|
import io
|
||||||
|
import copy
|
||||||
|
|
||||||
|
# Configuration du chemin de Tesseract OCR
|
||||||
|
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe' # Pour Windows
|
||||||
|
# Pour Linux et macOS, Tesseract doit être installé et disponible dans le PATH
|
||||||
|
|
||||||
|
class OCRProcessor:
|
||||||
|
"""
|
||||||
|
Classe pour traiter les images et extraire le texte
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, lang: str = "fra+eng"):
|
||||||
|
"""
|
||||||
|
Initialise le processeur OCR
|
||||||
|
|
||||||
|
Args:
|
||||||
|
lang (str): Langues pour la reconnaissance (fra pour français, eng pour anglais)
|
||||||
|
"""
|
||||||
|
self.lang = lang
|
||||||
|
|
||||||
|
def preprocess_image(self, image: Union[bytes, np.ndarray, str]) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
Prétraite l'image pour améliorer la reconnaissance OCR
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: Image sous forme de bytes, numpy array ou chemin de fichier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
np.ndarray: Image prétraitée
|
||||||
|
"""
|
||||||
|
# Convertir l'image en numpy array si nécessaire
|
||||||
|
if isinstance(image, bytes):
|
||||||
|
img = np.array(Image.open(io.BytesIO(image)))
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
||||||
|
elif isinstance(image, str):
|
||||||
|
img = cv2.imread(image)
|
||||||
|
else:
|
||||||
|
# Pour un np.ndarray, on peut utiliser copy() directement
|
||||||
|
# Pour d'autres types, on s'assure de créer une copie sécurisée
|
||||||
|
img = np.array(image) if not isinstance(image, np.ndarray) else image.copy()
|
||||||
|
|
||||||
|
# Convertir en niveaux de gris
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||||
|
|
||||||
|
# Appliquer une réduction de bruit
|
||||||
|
denoised = cv2.fastNlMeansDenoising(gray, None, 10, 7, 21)
|
||||||
|
|
||||||
|
# Seuillage adaptatif
|
||||||
|
binary = cv2.adaptiveThreshold(
|
||||||
|
denoised, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
cv2.THRESH_BINARY, 11, 2
|
||||||
|
)
|
||||||
|
|
||||||
|
return binary
|
||||||
|
|
||||||
|
def extract_text(self, image: Union[bytes, np.ndarray, str],
|
||||||
|
preprocess: bool = True) -> str:
|
||||||
|
"""
|
||||||
|
Extrait le texte d'une image
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: Image sous forme de bytes, numpy array ou chemin de fichier
|
||||||
|
preprocess (bool): Si True, prétraite l'image avant la reconnaissance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Texte extrait de l'image
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Prétraitement si demandé
|
||||||
|
if preprocess:
|
||||||
|
processed_img = self.preprocess_image(image)
|
||||||
|
else:
|
||||||
|
if isinstance(image, bytes):
|
||||||
|
processed_img = np.array(Image.open(io.BytesIO(image)))
|
||||||
|
elif isinstance(image, str):
|
||||||
|
processed_img = cv2.imread(image)
|
||||||
|
else:
|
||||||
|
processed_img = np.array(image) if not isinstance(image, np.ndarray) else image
|
||||||
|
|
||||||
|
# Lancer la reconnaissance OCR
|
||||||
|
config = r'--oem 3 --psm 6' # Page entière en mode 6
|
||||||
|
text = pytesseract.image_to_string(processed_img, lang=self.lang, config=config)
|
||||||
|
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de l'extraction de texte: {str(e)}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def extract_text_with_confidence(self, image: Union[bytes, np.ndarray, str],
|
||||||
|
preprocess: bool = True) -> Dict:
|
||||||
|
"""
|
||||||
|
Extrait le texte avec les données de confiance
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: Image sous forme de bytes, numpy array ou chemin de fichier
|
||||||
|
preprocess (bool): Si True, prétraite l'image avant la reconnaissance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict: Dictionnaire contenant le texte et les données de confiance
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Prétraitement si demandé
|
||||||
|
if preprocess:
|
||||||
|
processed_img = self.preprocess_image(image)
|
||||||
|
else:
|
||||||
|
if isinstance(image, bytes):
|
||||||
|
processed_img = np.array(Image.open(io.BytesIO(image)))
|
||||||
|
elif isinstance(image, str):
|
||||||
|
processed_img = cv2.imread(image)
|
||||||
|
else:
|
||||||
|
processed_img = np.array(image) if not isinstance(image, np.ndarray) else image
|
||||||
|
|
||||||
|
# Lancer la reconnaissance OCR avec données détaillées
|
||||||
|
config = r'--oem 3 --psm 6'
|
||||||
|
data = pytesseract.image_to_data(processed_img, lang=self.lang, config=config, output_type=pytesseract.Output.DICT)
|
||||||
|
|
||||||
|
# Calculer la confiance moyenne
|
||||||
|
confidences = [conf for conf in data['conf'] if conf != -1]
|
||||||
|
avg_confidence = sum(confidences) / len(confidences) if confidences else 0
|
||||||
|
|
||||||
|
# Reconstruire le texte à partir des mots reconnus
|
||||||
|
text = ' '.join([word for word in data['text'] if word.strip()])
|
||||||
|
|
||||||
|
return {
|
||||||
|
'text': text,
|
||||||
|
'confidence': avg_confidence,
|
||||||
|
'words': data['text'],
|
||||||
|
'word_confidences': data['conf']
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de l'extraction de texte avec confiance: {str(e)}")
|
||||||
|
return {'text': "", 'confidence': 0, 'words': [], 'word_confidences': []}
|
||||||
|
|
||||||
|
def detect_tables(self, image: Union[bytes, np.ndarray, str]) -> Optional[np.ndarray]:
|
||||||
|
"""
|
||||||
|
Détecte les tableaux dans une image
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: Image sous forme de bytes, numpy array ou chemin de fichier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[np.ndarray]: Image avec les tableaux identifiés ou None en cas d'erreur
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Convertir l'image en numpy array si nécessaire
|
||||||
|
if isinstance(image, bytes):
|
||||||
|
img = np.array(Image.open(io.BytesIO(image)))
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
||||||
|
elif isinstance(image, str):
|
||||||
|
img = cv2.imread(image)
|
||||||
|
else:
|
||||||
|
img = np.array(image) if not isinstance(image, np.ndarray) else image.copy()
|
||||||
|
|
||||||
|
# Convertir en niveaux de gris
|
||||||
|
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||||
|
|
||||||
|
# Seuillage
|
||||||
|
thresh = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||||
|
cv2.THRESH_BINARY_INV, 11, 2)
|
||||||
|
|
||||||
|
# Dilatation pour renforcer les lignes
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
|
||||||
|
dilate = cv2.dilate(thresh, kernel, iterations=3)
|
||||||
|
|
||||||
|
# Trouver les contours
|
||||||
|
contours, _ = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
|
||||||
|
# Dessiner les contours des tableaux potentiels sur une copie de l'image
|
||||||
|
result = img.copy()
|
||||||
|
|
||||||
|
for contour in contours:
|
||||||
|
area = cv2.contourArea(contour)
|
||||||
|
if area > 1000: # Filtrer les petits contours
|
||||||
|
x, y, w, h = cv2.boundingRect(contour)
|
||||||
|
# Un tableau a généralement un ratio largeur/hauteur spécifique
|
||||||
|
if 0.5 < w/h < 2: # Ajuster selon les besoins
|
||||||
|
cv2.rectangle(result, (x, y), (x+w, y+h), (0, 255, 0), 2)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de la détection de tableaux: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def extract_table_data(self, image: Union[bytes, np.ndarray, str]) -> Optional[Dict]:
|
||||||
|
"""
|
||||||
|
Extrait les données d'un tableau détecté dans l'image
|
||||||
|
|
||||||
|
Args:
|
||||||
|
image: Image sous forme de bytes, numpy array ou chemin de fichier
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[Dict]: Dictionnaire contenant les données du tableau ou None en cas d'erreur
|
||||||
|
"""
|
||||||
|
# Cette fonction est une simplification. Pour une reconnaissance complète de tableaux,
|
||||||
|
# des librairies spécialisées comme camelot-py ou tabula-py seraient plus appropriées.
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Pour cette démonstration, on utilise pytesseract avec un mode de segmentation adapté aux tableaux
|
||||||
|
if isinstance(image, bytes):
|
||||||
|
img = np.array(Image.open(io.BytesIO(image)))
|
||||||
|
elif isinstance(image, str):
|
||||||
|
img = cv2.imread(image)
|
||||||
|
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||||
|
else:
|
||||||
|
# Conversion sécurisée pour tout type d'entrée
|
||||||
|
img_array = np.array(image)
|
||||||
|
img = cv2.cvtColor(img_array, cv2.COLOR_BGR2RGB)
|
||||||
|
|
||||||
|
# Configuration pour tableaux
|
||||||
|
config = r'--oem 3 --psm 6 -c preserve_interword_spaces=1'
|
||||||
|
data = pytesseract.image_to_data(img, lang=self.lang, config=config, output_type=pytesseract.Output.DICT)
|
||||||
|
|
||||||
|
# Organiser les données en lignes et colonnes (simplification)
|
||||||
|
words = data['text']
|
||||||
|
left = data['left']
|
||||||
|
top = data['top']
|
||||||
|
|
||||||
|
# Identifier les lignes uniques basées sur la position verticale
|
||||||
|
unique_tops = sorted(list(set([t for t, w in zip(top, words) if w.strip()])))
|
||||||
|
rows = []
|
||||||
|
|
||||||
|
for t in unique_tops:
|
||||||
|
# Trouver tous les mots sur cette ligne (avec une tolérance)
|
||||||
|
tolerance = 10
|
||||||
|
row_words = [(l, w) for l, w, wt in zip(left, words, top)
|
||||||
|
if wt >= t-tolerance and wt <= t+tolerance and w.strip()]
|
||||||
|
|
||||||
|
# Trier les mots par position horizontale
|
||||||
|
row_words.sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
# Ajouter les mots de cette ligne
|
||||||
|
rows.append([w for _, w in row_words])
|
||||||
|
|
||||||
|
# Construire un dictionnaire de résultats
|
||||||
|
return {
|
||||||
|
'rows': rows,
|
||||||
|
'raw_text': ' '.join([word for word in words if word.strip()]),
|
||||||
|
'num_rows': len(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Erreur lors de l'extraction des données du tableau: {str(e)}")
|
||||||
|
return None
|
||||||
Loading…
x
Reference in New Issue
Block a user