martialroberge
commited on
Commit
·
3cb2c3b
1
Parent(s):
5e00169
Mise à jour pour utilisation avec clés API individuelles
Browse files- README.md +136 -7
- app.py +481 -0
- requirements.txt +7 -0
README.md
CHANGED
@@ -1,13 +1,142 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji:
|
4 |
-
colorFrom:
|
5 |
-
colorTo:
|
6 |
sdk: gradio
|
7 |
-
sdk_version:
|
8 |
app_file: app.py
|
9 |
pinned: false
|
10 |
-
license: apache-2.0
|
11 |
---
|
12 |
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
---
|
2 |
+
title: Mini-VLM Dataset Builder
|
3 |
+
emoji: 🎯
|
4 |
+
colorFrom: blue
|
5 |
+
colorTo: purple
|
6 |
sdk: gradio
|
7 |
+
sdk_version: 4.19.2
|
8 |
app_file: app.py
|
9 |
pinned: false
|
|
|
10 |
---
|
11 |
|
12 |
+
# Mini-VLM Dataset Builder
|
13 |
+
|
14 |
+
Cette application permet de créer facilement des datasets de questions/réponses pour l'entraînement de modèles Vision-Langage (VLM).
|
15 |
+
|
16 |
+
## ⚠️ Important : Clés API requises
|
17 |
+
|
18 |
+
Pour utiliser cette application, vous devez avoir :
|
19 |
+
1. Une clé API Google Gemini (obtenir [ici](https://makersuite.google.com/app/apikey))
|
20 |
+
2. Un token Hugging Face avec permissions d'écriture (obtenir [ici](https://huggingface.co/settings/tokens))
|
21 |
+
|
22 |
+
Ces clés sont à entrer directement dans l'interface de l'application. Elles ne sont jamais stockées et sont uniquement utilisées pour vos propres requêtes.
|
23 |
+
|
24 |
+
## Fonctionnalités
|
25 |
+
|
26 |
+
- 📸 Upload multiple d'images et de PDFs
|
27 |
+
- 🤖 Analyse automatique avec Gemini
|
28 |
+
- ❓ Génération de questions/réponses
|
29 |
+
- 📁 Dataset structuré (train/validation/test)
|
30 |
+
- ⬆️ Upload sur votre compte Hugging Face
|
31 |
+
|
32 |
+
## Utilisation
|
33 |
+
|
34 |
+
1. Entrez votre clé API Google Gemini dans le champ dédié
|
35 |
+
2. Entrez votre token Hugging Face dans le champ dédié
|
36 |
+
3. Spécifiez le nom de votre dataset (format: votre-username/nom-du-dataset)
|
37 |
+
4. Uploadez vos documents (PDF, PNG, JPG, JPEG)
|
38 |
+
5. Cliquez sur "Créer le dataset"
|
39 |
+
|
40 |
+
## Structure du dataset généré
|
41 |
+
|
42 |
+
Le dataset sera créé sur votre compte Hugging Face avec la structure suivante :
|
43 |
+
|
44 |
+
```
|
45 |
+
votre-username/nom-du-dataset/
|
46 |
+
├── train/
|
47 |
+
│ ├── images/
|
48 |
+
│ └── metadata.jsonl
|
49 |
+
├── validation/
|
50 |
+
│ ├── images/
|
51 |
+
│ └── metadata.jsonl
|
52 |
+
└── test/
|
53 |
+
├── images/
|
54 |
+
└── metadata.jsonl
|
55 |
+
```
|
56 |
+
|
57 |
+
## Sécurité
|
58 |
+
|
59 |
+
- Les clés API sont utilisées uniquement pendant votre session
|
60 |
+
- Aucune clé n'est stockée sur le serveur
|
61 |
+
- Les données sont transmises de manière sécurisée
|
62 |
+
- Chaque utilisateur utilise ses propres identifiants
|
63 |
+
|
64 |
+
## Licence
|
65 |
+
|
66 |
+
Apache License 2.0
|
67 |
+
|
68 |
+
## 🎨 Interface utilisateur moderne et intuitive
|
69 |
+
|
70 |
+
## 📊 Barre de progression en temps réel
|
71 |
+
|
72 |
+
## 📦 Installation
|
73 |
+
|
74 |
+
1. Clonez le repository :
|
75 |
+
```bash
|
76 |
+
git clone https://huggingface.co/spaces/Marsouuu/mini-vlm-dataset-builder
|
77 |
+
cd mini-vlm-dataset-builder
|
78 |
+
```
|
79 |
+
|
80 |
+
2. Installez les dépendances :
|
81 |
+
```bash
|
82 |
+
pip install -r requirements.txt
|
83 |
+
```
|
84 |
+
|
85 |
+
## 🚀 Utilisation
|
86 |
+
|
87 |
+
1. Lancez l'application :
|
88 |
+
```bash
|
89 |
+
python app.py
|
90 |
+
```
|
91 |
+
|
92 |
+
2. Accédez à l'interface web dans votre navigateur (généralement à l'adresse `http://localhost:7860`)
|
93 |
+
|
94 |
+
3. Dans l'interface :
|
95 |
+
- Entrez votre clé API Google Gemini
|
96 |
+
- Entrez votre token Hugging Face
|
97 |
+
- Choisissez un nom pour votre dataset
|
98 |
+
- Téléchargez vos images de documents
|
99 |
+
- Cliquez sur "Créer le dataset"
|
100 |
+
|
101 |
+
## 📁 Structure du dataset
|
102 |
+
|
103 |
+
Le dataset créé aura la structure suivante :
|
104 |
+
|
105 |
+
```
|
106 |
+
dataset_name/
|
107 |
+
├── train/
|
108 |
+
│ ├── images/
|
109 |
+
│ │ └── kid-page-{n}.png
|
110 |
+
│ └── metadata.jsonl
|
111 |
+
├── validation/
|
112 |
+
│ ├── images/
|
113 |
+
│ │ └── kid-page-{n}.png
|
114 |
+
│ └── metadata.jsonl
|
115 |
+
└── test/
|
116 |
+
├── images/
|
117 |
+
│ └── kid-page-{n}.png
|
118 |
+
└── metadata.jsonl
|
119 |
+
```
|
120 |
+
|
121 |
+
Chaque fichier `metadata.jsonl` contient des entrées au format :
|
122 |
+
```json
|
123 |
+
{
|
124 |
+
"image": "images/kid-page-{n}.png",
|
125 |
+
"query": "Question générée",
|
126 |
+
"answer": "Réponse générée",
|
127 |
+
"langue": "fr",
|
128 |
+
"page": 1,
|
129 |
+
"file_name": "images/kid-page-{n}.png"
|
130 |
+
}
|
131 |
+
```
|
132 |
+
|
133 |
+
## 🤝 Contribution
|
134 |
+
|
135 |
+
Les contributions sont les bienvenues ! N'hésitez pas à :
|
136 |
+
- Ouvrir une issue pour signaler un bug
|
137 |
+
- Proposer une amélioration via une pull request
|
138 |
+
- Partager vos idées d'amélioration
|
139 |
+
|
140 |
+
## 📝 Licence
|
141 |
+
|
142 |
+
Ce projet est sous licence MIT. Voir le fichier `LICENSE` pour plus de détails.
|
app.py
ADDED
@@ -0,0 +1,481 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
import shutil
|
5 |
+
from pathlib import Path
|
6 |
+
import google.generativeai as genai
|
7 |
+
from huggingface_hub import HfApi, create_repo
|
8 |
+
import tempfile
|
9 |
+
import uuid
|
10 |
+
from PIL import Image
|
11 |
+
import io
|
12 |
+
import hashlib
|
13 |
+
from typing import List, Dict, Set
|
14 |
+
import pdf2image
|
15 |
+
import fitz # PyMuPDF
|
16 |
+
from dotenv import load_dotenv
|
17 |
+
|
18 |
+
# Charger les variables d'environnement
|
19 |
+
load_dotenv()
|
20 |
+
|
21 |
+
# Récupérer les clés API depuis les variables d'environnement
|
22 |
+
DEFAULT_GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
|
23 |
+
DEFAULT_HF_TOKEN = os.getenv("HF_TOKEN", "")
|
24 |
+
|
25 |
+
def generate_unique_id() -> str:
|
26 |
+
"""Génère un ID unique de 50 caractères"""
|
27 |
+
# Générer deux UUID4 et les combiner
|
28 |
+
uuid1 = str(uuid.uuid4())
|
29 |
+
uuid2 = str(uuid.uuid4())
|
30 |
+
# Prendre les 25 premiers caractères de chaque UUID
|
31 |
+
return f"{uuid1[:25]}{uuid2[:25]}"
|
32 |
+
|
33 |
+
def check_duplicates(entries: List[Dict]) -> List[Dict]:
|
34 |
+
"""Vérifie et supprime les doublons dans les entrées du dataset"""
|
35 |
+
seen_questions = set()
|
36 |
+
seen_images = set()
|
37 |
+
unique_entries = []
|
38 |
+
|
39 |
+
for entry in entries:
|
40 |
+
# Créer une clé unique basée sur la question et l'image
|
41 |
+
question_key = entry['query'].lower().strip()
|
42 |
+
image_key = entry['image']
|
43 |
+
|
44 |
+
# Vérifier si la question est similaire à une question existante
|
45 |
+
is_duplicate = False
|
46 |
+
for seen_question in seen_questions:
|
47 |
+
# Calculer la similarité entre les questions
|
48 |
+
if question_key in seen_question or seen_question in question_key:
|
49 |
+
is_duplicate = True
|
50 |
+
break
|
51 |
+
|
52 |
+
# Vérifier si l'image a déjà été utilisée trop de fois
|
53 |
+
image_count = sum(1 for e in unique_entries if e['image'] == image_key)
|
54 |
+
|
55 |
+
if not is_duplicate and image_count < 5: # Limite à 5 questions par image
|
56 |
+
seen_questions.add(question_key)
|
57 |
+
seen_images.add(image_key)
|
58 |
+
unique_entries.append(entry)
|
59 |
+
|
60 |
+
return unique_entries
|
61 |
+
|
62 |
+
def process_files(api_key: str, hf_token: str, files: List[str], dataset_name: str, progress=gr.Progress()) -> str:
|
63 |
+
"""
|
64 |
+
Traite les fichiers (images ou PDFs) et crée le dataset
|
65 |
+
"""
|
66 |
+
try:
|
67 |
+
print(f"🔍 Début du traitement avec {len(files)} fichiers")
|
68 |
+
|
69 |
+
# Dossier temporaire pour toutes les images avant mélange
|
70 |
+
with tempfile.TemporaryDirectory() as temp_images_dir:
|
71 |
+
print("📂 Création du dossier temporaire pour les images")
|
72 |
+
|
73 |
+
# Compteur pour numéroter les images
|
74 |
+
image_counter = 1
|
75 |
+
|
76 |
+
# Liste pour stocker tous les chemins d'images
|
77 |
+
all_images = []
|
78 |
+
|
79 |
+
# Traiter d'abord tous les PDFs
|
80 |
+
print("📄 Traitement des PDFs...")
|
81 |
+
for file in files:
|
82 |
+
if file.name.lower().endswith('.pdf'):
|
83 |
+
print(f"📄 Conversion du PDF: {file.name}")
|
84 |
+
try:
|
85 |
+
# Ouvrir le PDF avec PyMuPDF
|
86 |
+
pdf_document = fitz.open(file.name)
|
87 |
+
|
88 |
+
# Convertir chaque page en image
|
89 |
+
for page_num in range(len(pdf_document)):
|
90 |
+
page = pdf_document[page_num]
|
91 |
+
pix = page.get_pixmap()
|
92 |
+
image_path = os.path.join(temp_images_dir, f"{image_counter}.png")
|
93 |
+
pix.save(image_path)
|
94 |
+
all_images.append(image_path)
|
95 |
+
print(f" 📄 Page {page_num + 1} convertie en {image_counter}.png")
|
96 |
+
image_counter += 1
|
97 |
+
|
98 |
+
pdf_document.close()
|
99 |
+
except Exception as e:
|
100 |
+
print(f"❌ Erreur lors de la conversion du PDF {file.name}: {str(e)}")
|
101 |
+
continue
|
102 |
+
|
103 |
+
# Traiter ensuite toutes les images
|
104 |
+
print("🖼️ Traitement des images...")
|
105 |
+
for file in files:
|
106 |
+
file_lower = file.name.lower()
|
107 |
+
if file_lower.endswith(('.png', '.jpg', '.jpeg')):
|
108 |
+
try:
|
109 |
+
# Copier et renommer l'image
|
110 |
+
new_path = os.path.join(temp_images_dir, f"{image_counter}.png")
|
111 |
+
# Convertir en PNG si nécessaire
|
112 |
+
if file_lower.endswith(('.jpg', '.jpeg')):
|
113 |
+
img = Image.open(file.name)
|
114 |
+
img.save(new_path, 'PNG')
|
115 |
+
else:
|
116 |
+
shutil.copy2(file.name, new_path)
|
117 |
+
all_images.append(new_path)
|
118 |
+
print(f"🖼️ Image {file.name} copiée en {image_counter}.png")
|
119 |
+
image_counter += 1
|
120 |
+
except Exception as e:
|
121 |
+
print(f"❌ Erreur lors du traitement de l'image {file.name}: {str(e)}")
|
122 |
+
continue
|
123 |
+
|
124 |
+
if not all_images:
|
125 |
+
return "❌ Erreur: Aucune image valide trouvée. Veuillez fournir des fichiers PDF ou des images (PNG, JPG, JPEG)."
|
126 |
+
|
127 |
+
# Mélanger toutes les images
|
128 |
+
print(f"🔄 Mélange des {len(all_images)} images...")
|
129 |
+
import random
|
130 |
+
random.shuffle(all_images)
|
131 |
+
|
132 |
+
# Créer un nouveau dossier pour les images mélangées et renumérotées
|
133 |
+
with tempfile.TemporaryDirectory() as final_images_dir:
|
134 |
+
final_images = []
|
135 |
+
for i, image_path in enumerate(all_images, 1):
|
136 |
+
new_path = os.path.join(final_images_dir, f"{i}.png")
|
137 |
+
shutil.copy2(image_path, new_path)
|
138 |
+
final_images.append(new_path)
|
139 |
+
print(f"📝 Image {os.path.basename(image_path)} renumérotée en {i}.png")
|
140 |
+
|
141 |
+
print(f"✅ Total des images à traiter: {len(final_images)}")
|
142 |
+
|
143 |
+
# Continuer avec le traitement des images
|
144 |
+
return process_images(api_key, hf_token, final_images, dataset_name, progress)
|
145 |
+
|
146 |
+
except Exception as e:
|
147 |
+
return f"❌ Erreur lors du traitement des fichiers: {str(e)}"
|
148 |
+
|
149 |
+
def process_images(api_key, hf_token, images, dataset_name, progress=gr.Progress()):
|
150 |
+
"""
|
151 |
+
Traite les images et crée le dataset
|
152 |
+
"""
|
153 |
+
try:
|
154 |
+
print(f"🔍 Début du traitement avec {len(images)} images")
|
155 |
+
print(f"🔑 API Key: {api_key[:5]}...")
|
156 |
+
print(f"🔑 HF Token: {hf_token[:5]}...")
|
157 |
+
print(f"📁 Dataset name: {dataset_name}")
|
158 |
+
|
159 |
+
if not api_key or not hf_token:
|
160 |
+
print("❌ Erreur: API Key ou HF Token manquant")
|
161 |
+
return "❌ Erreur: Veuillez entrer votre clé API Google Gemini et votre token Hugging Face."
|
162 |
+
|
163 |
+
# Configuration de l'API Gemini
|
164 |
+
print("⚙️ Configuration de l'API Gemini...")
|
165 |
+
genai.configure(api_key=api_key)
|
166 |
+
model = genai.GenerativeModel('gemini-1.5-flash')
|
167 |
+
|
168 |
+
# Créer d'abord le repository sur Hugging Face
|
169 |
+
try:
|
170 |
+
print("📦 Création du repository sur Hugging Face...")
|
171 |
+
api = HfApi(token=hf_token)
|
172 |
+
create_repo(dataset_name, repo_type="dataset", token=hf_token, exist_ok=True)
|
173 |
+
print("✅ Repository créé avec succès")
|
174 |
+
except Exception as e:
|
175 |
+
print(f"❌ Erreur lors de la création du repository: {str(e)}")
|
176 |
+
return f"❌ Erreur lors de la création du repository sur Hugging Face: {str(e)}"
|
177 |
+
|
178 |
+
# Création d'un dossier temporaire pour le dataset
|
179 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
180 |
+
print(f"📂 Création du dossier temporaire: {temp_dir}")
|
181 |
+
# Extraire le nom du dataset sans le nom d'utilisateur
|
182 |
+
repo_name = dataset_name.split('/')[-1]
|
183 |
+
dataset_path = Path(temp_dir) / repo_name
|
184 |
+
dataset_path.mkdir()
|
185 |
+
|
186 |
+
# Création des dossiers pour les splits
|
187 |
+
splits = ['train', 'validation', 'test']
|
188 |
+
for split in splits:
|
189 |
+
(dataset_path / split / 'images').mkdir(parents=True)
|
190 |
+
print(f"📁 Création du dossier {split}")
|
191 |
+
|
192 |
+
# Mélanger les images aléatoirement
|
193 |
+
import random
|
194 |
+
random.shuffle(images)
|
195 |
+
|
196 |
+
total_images = len(images)
|
197 |
+
progress(0, desc="Démarrage du traitement...")
|
198 |
+
|
199 |
+
# Créer un dossier temporaire pour les images renommées
|
200 |
+
with tempfile.TemporaryDirectory() as renamed_images_dir:
|
201 |
+
renamed_images = []
|
202 |
+
print("🔄 Renommage des images...")
|
203 |
+
|
204 |
+
# Renommer et copier toutes les images d'abord
|
205 |
+
for i, image in enumerate(images, 1):
|
206 |
+
new_image_path = Path(renamed_images_dir) / f"{i}.png"
|
207 |
+
shutil.copy2(image, new_image_path)
|
208 |
+
renamed_images.append(str(new_image_path))
|
209 |
+
print(f"📝 Image {image} renommée en {i}.png")
|
210 |
+
|
211 |
+
# Traitement des images renommées
|
212 |
+
for i, image in enumerate(renamed_images, 1):
|
213 |
+
print(f"\n🖼️ Traitement de l'image {i}/{total_images}")
|
214 |
+
progress(i / total_images, desc=f"Traitement de l'image {i}/{total_images}")
|
215 |
+
|
216 |
+
# Déterminer le split (80% train, 10% validation, 10% test)
|
217 |
+
if i <= len(images) * 0.8:
|
218 |
+
split = 'train'
|
219 |
+
elif i <= len(images) * 0.9:
|
220 |
+
split = 'validation'
|
221 |
+
else:
|
222 |
+
split = 'test'
|
223 |
+
|
224 |
+
print(f"📂 Split: {split}")
|
225 |
+
|
226 |
+
# Copier l'image
|
227 |
+
image_path = dataset_path / split / 'images' / f"{i}.png"
|
228 |
+
print(f"📄 Copie de l'image vers: {image_path}")
|
229 |
+
shutil.copy2(image, image_path)
|
230 |
+
|
231 |
+
# Générer les questions/réponses avec Gemini
|
232 |
+
all_qa_pairs = []
|
233 |
+
|
234 |
+
# Une seule tentative par image
|
235 |
+
with open(image, 'rb') as img_file:
|
236 |
+
img_data = img_file.read()
|
237 |
+
|
238 |
+
# Générer un nombre aléatoire de questions à poser
|
239 |
+
nb_questions = random.randint(1, 5)
|
240 |
+
print(f"❓ Génération de {nb_questions} questions...")
|
241 |
+
|
242 |
+
prompt = f"""Tu es un expert en analyse financière et en création de datasets de haute qualité. Examine attentivement ce document financier et génère exactement {nb_questions} questions/réponses de qualité professionnelle.
|
243 |
+
|
244 |
+
Format de réponse requis (JSON) :
|
245 |
+
[
|
246 |
+
{{
|
247 |
+
"query": "Question financière précise et détaillée",
|
248 |
+
"answer": "Réponse complète et exacte basée sur le document",
|
249 |
+
"langue": "fr",
|
250 |
+
"is_negative": false
|
251 |
+
}}
|
252 |
+
]
|
253 |
+
|
254 |
+
Instructions détaillées pour la création du dataset :
|
255 |
+
|
256 |
+
1. TYPES DE QUESTIONS FINANCIÈRES (sois créatif et précis) :
|
257 |
+
- Analyse quantitative :
|
258 |
+
* Montants exacts et variations
|
259 |
+
* Pourcentages et ratios financiers
|
260 |
+
* Évolutions temporelles
|
261 |
+
* Comparaisons chiffrées
|
262 |
+
|
263 |
+
- Analyse qualitative :
|
264 |
+
* Stratégies et objectifs
|
265 |
+
* Risques et opportunités
|
266 |
+
* Contexte réglementaire
|
267 |
+
* Implications business
|
268 |
+
|
269 |
+
- Dates et échéances :
|
270 |
+
* Périodes de reporting
|
271 |
+
* Dates clés
|
272 |
+
* Échéances importantes
|
273 |
+
* Historique des événements
|
274 |
+
|
275 |
+
2. QUESTIONS NÉGATIVES (TRÈS IMPORTANT) :
|
276 |
+
- Tu DOIS générer au moins 1 question sur {nb_questions} où l'information n'est PAS dans le document
|
277 |
+
- Pour ces questions, tu DOIS mettre "is_negative": true
|
278 |
+
- La réponse DOIT commencer par "Cette information ne figure pas dans le document"
|
279 |
+
- Exemples de questions négatives :
|
280 |
+
* "Quel est le montant exact des provisions pour risques ?" -> "Cette information ne figure pas dans le document"
|
281 |
+
* "Quelle est la rémunération du directeur financier ?" -> "Cette information ne figure pas dans le document"
|
282 |
+
- Les questions négatives doivent être plausibles et pertinentes pour un document financier
|
283 |
+
|
284 |
+
3. QUALITÉ DES QUESTIONS :
|
285 |
+
- Précision : utilise des chiffres exacts quand possible
|
286 |
+
- Clarté : questions non ambiguës
|
287 |
+
- Pertinence : focus sur les aspects financiers importants
|
288 |
+
- Variété : mélange différents types de questions
|
289 |
+
- Profondeur : questions qui nécessitent une analyse approfondie
|
290 |
+
|
291 |
+
4. QUALITÉ DES RÉPONSES :
|
292 |
+
- Pour les questions normales (is_negative: false) :
|
293 |
+
* Exactitude : informations vérifiables dans le document
|
294 |
+
* Complétude : réponses détaillées et exhaustives
|
295 |
+
* Clarté : formulation professionnelle et précise
|
296 |
+
* Contexte : inclure les éléments de contexte pertinents
|
297 |
+
- Pour les questions négatives (is_negative: true) :
|
298 |
+
* TOUJOURS commencer par "Cette information ne figure pas dans le document"
|
299 |
+
* Expliquer brièvement pourquoi cette information serait pertinente
|
300 |
+
|
301 |
+
5. RÈGLES STRICTES :
|
302 |
+
- Questions et réponses UNIQUEMENT en français
|
303 |
+
- Pas de questions vagues ou générales
|
304 |
+
- Pas de répétition de questions similaires
|
305 |
+
- Pas de devinettes ou d'inférences non documentées
|
306 |
+
- Respect strict du format JSON demandé
|
307 |
+
- Au moins 1 question négative (is_negative: true) par image
|
308 |
+
|
309 |
+
La réponse doit être uniquement le JSON, sans texte supplémentaire."""
|
310 |
+
|
311 |
+
response = model.generate_content([
|
312 |
+
prompt,
|
313 |
+
{"mime_type": "image/png", "data": img_data}
|
314 |
+
])
|
315 |
+
|
316 |
+
try:
|
317 |
+
# Nettoyer la réponse pour ne garder que le JSON
|
318 |
+
response_text = response.text.strip()
|
319 |
+
if response_text.startswith("```json"):
|
320 |
+
response_text = response_text[7:]
|
321 |
+
if response_text.endswith("```"):
|
322 |
+
response_text = response_text[:-3]
|
323 |
+
response_text = response_text.strip()
|
324 |
+
|
325 |
+
# Extraire et formater les Q/R
|
326 |
+
qa_pairs = json.loads(response_text)
|
327 |
+
|
328 |
+
# Vérifier que c'est une liste
|
329 |
+
if not isinstance(qa_pairs, list):
|
330 |
+
raise ValueError("La réponse n'est pas une liste")
|
331 |
+
|
332 |
+
# Vérifier qu'il y a au moins une question négative
|
333 |
+
has_negative = False
|
334 |
+
for qa in qa_pairs:
|
335 |
+
if qa.get("is_negative", False):
|
336 |
+
if not qa["answer"].startswith("Cette information ne figure pas dans le document"):
|
337 |
+
qa["answer"] = "Cette information ne figure pas dans le document. " + qa["answer"]
|
338 |
+
has_negative = True
|
339 |
+
|
340 |
+
if not has_negative:
|
341 |
+
print("⚠️ Attention: Aucune question négative générée, on réessaie...")
|
342 |
+
continue
|
343 |
+
|
344 |
+
# Vérifier que chaque élément a les bons champs
|
345 |
+
for qa in qa_pairs:
|
346 |
+
if not all(key in qa for key in ["query", "answer", "langue", "is_negative"]):
|
347 |
+
raise ValueError("Un élément ne contient pas tous les champs requis")
|
348 |
+
|
349 |
+
# Vérifier que la langue est fr
|
350 |
+
if qa["langue"] != "fr":
|
351 |
+
qa["langue"] = "fr"
|
352 |
+
|
353 |
+
# Générer un ID unique
|
354 |
+
qa["id"] = generate_unique_id()
|
355 |
+
|
356 |
+
# Ajouter le chemin de l'image avec le nouveau nom
|
357 |
+
qa["image"] = f"images/{i}.png"
|
358 |
+
qa["file_name"] = f"images/{i}.png"
|
359 |
+
|
360 |
+
all_qa_pairs.extend(qa_pairs)
|
361 |
+
|
362 |
+
except json.JSONDecodeError as e:
|
363 |
+
print(f"Erreur JSON: {str(e)}")
|
364 |
+
continue
|
365 |
+
except ValueError as e:
|
366 |
+
print(f"Erreur de format: {str(e)}")
|
367 |
+
continue
|
368 |
+
|
369 |
+
# Vérifier et supprimer les doublons
|
370 |
+
unique_qa_pairs = check_duplicates(all_qa_pairs)
|
371 |
+
|
372 |
+
# Créer les entrées pour le JSONL
|
373 |
+
for qa in unique_qa_pairs:
|
374 |
+
entry = {
|
375 |
+
"id": qa["id"],
|
376 |
+
"image": qa["image"],
|
377 |
+
"query": qa["query"],
|
378 |
+
"answer": qa["answer"],
|
379 |
+
"langue": qa["langue"],
|
380 |
+
"file_name": qa["file_name"],
|
381 |
+
"is_negative": qa["is_negative"]
|
382 |
+
}
|
383 |
+
|
384 |
+
# Ajouter au fichier JSONL correspondant
|
385 |
+
jsonl_path = dataset_path / split / "metadata.jsonl"
|
386 |
+
with open(jsonl_path, 'a', encoding='utf-8') as f:
|
387 |
+
f.write(json.dumps(entry, ensure_ascii=False) + '\n')
|
388 |
+
|
389 |
+
# Créer le fichier LICENSE
|
390 |
+
with open(dataset_path / "LICENSE", 'w') as f:
|
391 |
+
f.write("""Apache License 2.0
|
392 |
+
|
393 |
+
Copyright [yyyy] [name of copyright owner]
|
394 |
+
|
395 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
396 |
+
you may not use this file except in compliance with the License.
|
397 |
+
You may obtain a copy of the License at
|
398 |
+
|
399 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
400 |
+
|
401 |
+
Unless required by applicable law or agreed to in writing, software
|
402 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
403 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
404 |
+
See the License for the specific language governing permissions and
|
405 |
+
limitations under the License.
|
406 |
+
""")
|
407 |
+
|
408 |
+
progress(0.9, desc="Upload du dataset sur Hugging Face...")
|
409 |
+
|
410 |
+
try:
|
411 |
+
# Uploader le dataset
|
412 |
+
api.upload_folder(
|
413 |
+
folder_path=str(dataset_path),
|
414 |
+
repo_id=dataset_name,
|
415 |
+
repo_type="dataset"
|
416 |
+
)
|
417 |
+
|
418 |
+
progress(1.0, desc="Terminé !")
|
419 |
+
return f"✅ Dataset créé avec succès !\n\nAccédez à votre dataset : https://huggingface.co/datasets/{dataset_name}"
|
420 |
+
|
421 |
+
except Exception as e:
|
422 |
+
return f"❌ Erreur lors de l'upload du dataset sur Hugging Face: {str(e)}"
|
423 |
+
|
424 |
+
except Exception as e:
|
425 |
+
return f"❌ Erreur: {str(e)}"
|
426 |
+
|
427 |
+
# Interface Gradio
|
428 |
+
with gr.Blocks() as demo:
|
429 |
+
gr.Markdown("""
|
430 |
+
# 🎯 Mini-VLM Dataset Builder
|
431 |
+
Créez votre propre dataset de questions/réponses pour l'entraînement de modèles Vision-Langage
|
432 |
+
|
433 |
+
### ⚠️ Important
|
434 |
+
1. Vous devez avoir une [clé API Gemini](https://makersuite.google.com/app/apikey)
|
435 |
+
2. Vous devez avoir un [token Hugging Face](https://huggingface.co/settings/tokens) avec droits d'écriture
|
436 |
+
""")
|
437 |
+
|
438 |
+
with gr.Row():
|
439 |
+
with gr.Column(scale=1):
|
440 |
+
api_key = gr.Textbox(
|
441 |
+
label="Clé API Google Gemini",
|
442 |
+
type="password",
|
443 |
+
placeholder="Entrez votre clé API Gemini",
|
444 |
+
value=""
|
445 |
+
)
|
446 |
+
hf_token = gr.Textbox(
|
447 |
+
label="Token Hugging Face",
|
448 |
+
type="password",
|
449 |
+
placeholder="Entrez votre token Hugging Face",
|
450 |
+
value=""
|
451 |
+
)
|
452 |
+
dataset_name = gr.Textbox(
|
453 |
+
label="Nom du dataset",
|
454 |
+
placeholder="votre-username/nom-du-dataset"
|
455 |
+
)
|
456 |
+
|
457 |
+
with gr.Column(scale=1):
|
458 |
+
files = gr.File(
|
459 |
+
label="Documents (PDF, PNG, JPG, JPEG)",
|
460 |
+
file_count="multiple",
|
461 |
+
height=200
|
462 |
+
)
|
463 |
+
|
464 |
+
submit_btn = gr.Button("Créer le dataset", variant="primary")
|
465 |
+
output = gr.Textbox(
|
466 |
+
label="Résultat",
|
467 |
+
lines=3,
|
468 |
+
interactive=False
|
469 |
+
)
|
470 |
+
|
471 |
+
submit_btn.click(
|
472 |
+
fn=process_files,
|
473 |
+
inputs=[api_key, hf_token, files, dataset_name],
|
474 |
+
outputs=output
|
475 |
+
)
|
476 |
+
|
477 |
+
if __name__ == "__main__":
|
478 |
+
demo.launch()
|
479 |
+
else:
|
480 |
+
# Pour Hugging Face Spaces
|
481 |
+
app = demo
|
requirements.txt
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
gradio>=4.19.2
|
2 |
+
google-generativeai
|
3 |
+
huggingface_hub
|
4 |
+
Pillow
|
5 |
+
PyMuPDF
|
6 |
+
pdf2image
|
7 |
+
python-dotenv
|