devalender commited on
Commit
2f1c164
·
verified ·
1 Parent(s): e15b9b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +105 -86
app.py CHANGED
@@ -1,145 +1,164 @@
1
- import os # <-- N'oublie pas d'importer os pour os.getenv !
2
  import gradio as gr
3
- import requests
4
  import base64
5
  import io
 
6
  from PIL import Image
7
- import fitz # PyMuPDF pour gérer les PDFs
 
 
 
 
 
 
 
 
8
 
9
- # --- CONFIGURATION DE L'API MISTRAL ---
10
- # Récupère la clé depuis les secrets (sécurisé)
11
- MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY")
12
- if not MISTRAL_API_KEY:
13
- raise ValueError("❌ Clé API Mistral manquante. Ajoutez-la dans les 'Repository Secrets' de votre Space Hugging Face.")
14
- API_URL = "https://api.mistral.ai/v1/chat/completions"
15
 
16
  # --- FONCTIONS AUXILIAIRES ---
17
- def encode_image(image):
18
- """Convertit une image PIL en base64 pour l'envoi à l'API."""
19
- buffer = io.BytesIO()
20
- image.save(buffer, format="JPEG") # Convertit en JPEG
21
- return base64.b64encode(buffer.getvalue()).decode() # Encode en base64
22
 
23
- def pdf_to_image(pdf_path):
24
- """
25
- Convertit la première page d'un PDF en image PIL.
26
- Args:
27
- pdf_path (str): Chemin vers le fichier PDF.
28
- Returns:
29
- PIL.Image: Image de la première page du PDF.
30
- """
31
- doc = fitz.open(pdf_path) # Ouvre le PDF
32
- first_page = doc.load_page(0) # Charge la première page (index 0)
33
- pix = first_page.get_pixmap() # Récupère les pixels de la page
34
- img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples) # Crée une image PIL
 
 
 
35
  return img
36
 
 
 
 
 
 
 
 
 
 
37
  # --- FONCTION PRINCIPALE ---
38
- def generate_filename(file, custom_prompt):
 
39
  """
40
- Génère un nom de fichier structuré à partir d'une image ou d'un PDF (première page uniquement).
41
  Args:
42
- file (gr.File): Fichier uploadé (image ou PDF).
43
- custom_prompt (str): Format personnalisé pour le nom de fichier (optionnel).
44
  Returns:
45
- str: Nom de fichier généré ou message d'erreur.
46
  """
47
  # 1. Vérification du fichier
48
  if file is None:
49
- return "❌ Veuillez uploader un fichier (image ou PDF)."
50
-
51
- # 2. Vérification de la clé API
52
- if not MISTRAL_API_KEY or MISTRAL_API_KEY == "VOTRE_CLE_API_MISTRAL":
53
- return "❌ Clé API manquante. Obtenez une clé gratuite sur [console.mistral.ai](https://console.mistral.ai)"
54
 
55
  try:
56
- # 3. Traitement du fichier (image ou PDF)
57
- if file.name.lower().endswith('.pdf'):
58
- # Cas PDF : extraire la première page et la convertir en image
59
  image = pdf_to_image(file.name)
60
  else:
61
- # Cas image : ouvrir directement
62
- image = Image.open(file.name)
63
 
64
- # 4. Encodage de l'image en base64
65
  image_base64 = encode_image(image)
66
 
67
- # 5. Préparation du prompt pour Mistral
68
  if custom_prompt and custom_prompt.strip():
69
- prompt = custom_prompt.strip() # Utilise le format personnalisé
70
  else:
71
- prompt = """Analyse ce document. Extrait le nom des parties et la date.
72
- Génère un nom de fichier au format: Affaire_[X]_contre_[Y]_[JJ_MM_AAAA]
73
- Exemple: Affaire_Dupont_contre_Martin_12_12_2026
74
- Réponds uniquement par le nom de fichier, rien d'autre."""
 
75
 
76
- # 6. Appel à l'API Mistral
 
77
  response = requests.post(
78
  API_URL,
79
  headers={
80
- "Authorization": f"Bearer {MISTRAL_API_KEY}",
81
- "Content-Type": "application/json"
82
  },
83
  json={
84
- "model": "pixtral-12b-2409",
85
- "messages": [{
86
- "role": "user",
87
- "content": [
88
- {"type": "text", "text": prompt},
89
- {"type": "image_url", "image_url": f"data:image/jpeg;base64,{image_base64}"}
90
- ]
91
- }],
92
- "max_tokens": 100,
93
- "temperature": 0.1
 
 
 
 
 
 
 
94
  },
95
- timeout=60
96
  )
97
 
98
- # 7. Traitement de la réponse
99
  if response.status_code == 200:
100
- result = response.json()
101
- return result["choices"][0]["message"]["content"].strip()
 
102
  else:
103
- return f"❌ Erreur API Mistral: Code {response.status_code} - {response.text}"
104
 
105
  except Exception as e:
106
- return f"❌ Erreur: {str(e)}"
 
107
 
108
  # --- INTERFACE GRADIO ---
109
- with gr.Blocks(title="Nommage de documents juridiques") as demo:
 
110
  gr.Markdown("""
111
- # ⚖️ Générateur de noms pour documents juridiques
112
- **Basé sur Mistral Pixtral** - Gratuit
113
- Upload un **fichier (image ou PDF)** → la première page sera analysée pour générer un nom structuré.
114
  """)
115
 
116
  with gr.Row():
117
  with gr.Column():
118
- # Champ pour uploader un fichier (image ou PDF)
119
- file = gr.File(
120
  label="Document (Image ou PDF)",
121
  file_types=[".jpg", ".jpeg", ".png", ".pdf"],
122
- height=400
123
  )
124
- # Champ pour le format personnalisé
125
- custom_prompt = gr.Textbox(
126
  label="Format personnalisé (optionnel)",
127
- placeholder='Ex: "Format: DOS_[X]_VS_[Y]_[DDMMYYYY]"',
128
- lines=2
129
  )
130
- # Bouton pour lancer l'analyse
131
- btn = gr.Button("Générer le nom", variant="primary")
132
 
133
  with gr.Column():
134
- # Zone pour afficher le résultat
135
- output = gr.Textbox(label="Nom généré", lines=5)
136
 
137
- # Lien entre le bouton et la fonction
138
  btn.click(
139
  fn=generate_filename,
140
- inputs=[file, custom_prompt],
141
- outputs=output
 
 
 
 
 
 
142
  )
143
 
144
- # --- LANCEMENT DE L'APPLICATION ---
145
  demo.launch(show_error=True)
 
1
+ import os
2
  import gradio as gr
 
3
  import base64
4
  import io
5
+ import re
6
  from PIL import Image
7
+ import fitz # PyMuPDF
8
+
9
+ # --- CONFIGURATION ---
10
+ # Récupère la clé HF depuis les secrets du Space (sécurisé)
11
+ HF_TOKEN = os.getenv("HF_TOKEN")
12
+ if not HF_TOKEN:
13
+ raise ValueError(
14
+ "❌ Token HF manquant. Ajoutez HF_TOKEN dans les 'Repository Secrets' de votre Space."
15
+ )
16
 
17
+ MODEL_ID = "google/gemma-4-E4B-it"
18
+ API_URL = f"https://api-inference.huggingface.co/models/{MODEL_ID}/v1/chat/completions"
 
 
 
 
19
 
20
  # --- FONCTIONS AUXILIAIRES ---
 
 
 
 
 
21
 
22
+ def encode_image(image: Image.Image) -> str:
23
+ """Convertit une image PIL en base64 JPEG."""
24
+ buffer = io.BytesIO()
25
+ image.save(buffer, format="JPEG", quality=85)
26
+ return base64.b64encode(buffer.getvalue()).decode()
27
+
28
+
29
+ def pdf_to_image(pdf_path: str) -> Image.Image:
30
+ """Convertit la première page d'un PDF en image PIL."""
31
+ doc = fitz.open(pdf_path)
32
+ page = doc.load_page(0)
33
+ mat = fitz.Matrix(150 / 72, 150 / 72) # 150 DPI lisible et léger
34
+ pix = page.get_pixmap(matrix=mat)
35
+ img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
36
+ doc.close()
37
  return img
38
 
39
+
40
+ def sanitize_filename(name: str) -> str:
41
+ """Nettoie le nom retourné par le modèle pour en faire un nom de fichier valide."""
42
+ name = name.strip().strip('"').strip("'")
43
+ name = re.sub(r"[^\w\s\-]", "", name)
44
+ name = re.sub(r"\s+", "_", name)
45
+ return name[:80] if name else "document_sans_nom"
46
+
47
+
48
  # --- FONCTION PRINCIPALE ---
49
+
50
+ def generate_filename(file, custom_prompt: str):
51
  """
52
+ Génère un nom de fichier à partir d'une image ou d'un PDF (première page).
53
  Args:
54
+ file : Fichier uploadé (image ou PDF).
55
+ custom_prompt : Prompt/format personnalisé pour le nom.
56
  Returns:
57
+ tuple: (aperçu PIL Image, nom généré str)
58
  """
59
  # 1. Vérification du fichier
60
  if file is None:
61
+ return None, "❌ Veuillez uploader un fichier (image ou PDF)."
 
 
 
 
62
 
63
  try:
64
+ # 2. Traitement selon le type de fichier
65
+ if file.name.lower().endswith(".pdf"):
 
66
  image = pdf_to_image(file.name)
67
  else:
68
+ image = Image.open(file.name).convert("RGB")
 
69
 
70
+ # 3. Encodage base64
71
  image_base64 = encode_image(image)
72
 
73
+ # 4. Prompt
74
  if custom_prompt and custom_prompt.strip():
75
+ prompt = custom_prompt.strip()
76
  else:
77
+ prompt = (
78
+ "Analyse ce document. Génère un nom de fichier court et descriptif "
79
+ "en snake_case (3 à 6 mots max).\n"
80
+ "Réponds UNIQUEMENT par le nom de fichier, sans extension, sans explication."
81
+ )
82
 
83
+ # 5. Appel à l'Inference API HF (même structure que ton ancien script Mistral)
84
+ import requests
85
  response = requests.post(
86
  API_URL,
87
  headers={
88
+ "Authorization": f"Bearer {HF_TOKEN}",
89
+ "Content-Type": "application/json",
90
  },
91
  json={
92
+ "model": MODEL_ID,
93
+ "messages": [
94
+ {
95
+ "role": "user",
96
+ "content": [
97
+ {"type": "text", "text": prompt},
98
+ {
99
+ "type": "image_url",
100
+ "image_url": {
101
+ "url": f"data:image/jpeg;base64,{image_base64}"
102
+ },
103
+ },
104
+ ],
105
+ }
106
+ ],
107
+ "max_tokens": 60,
108
+ "temperature": 0.1,
109
  },
110
+ timeout=60,
111
  )
112
 
113
+ # 6. Traitement de la réponse
114
  if response.status_code == 200:
115
+ raw = response.json()["choices"][0]["message"]["content"].strip()
116
+ clean = sanitize_filename(raw)
117
+ return image, f"{clean}.pdf"
118
  else:
119
+ return image, f"❌ Erreur API HF : {response.status_code} {response.text}"
120
 
121
  except Exception as e:
122
+ return None, f"❌ Erreur : {str(e)}"
123
+
124
 
125
  # --- INTERFACE GRADIO ---
126
+
127
+ with gr.Blocks(title="PDF Auto-Namer") as demo:
128
  gr.Markdown("""
129
+ # 📄 PDF Auto-Namer
130
+ **Basé sur Gemma 4 E4B (multimodal)** via Hugging Face Inference API
131
+ Upload un **fichier image ou PDF** → la première page est analysée un nom est généré.
132
  """)
133
 
134
  with gr.Row():
135
  with gr.Column():
136
+ file_input = gr.File(
 
137
  label="Document (Image ou PDF)",
138
  file_types=[".jpg", ".jpeg", ".png", ".pdf"],
139
+ height=200,
140
  )
141
+ prompt_input = gr.Textbox(
 
142
  label="Format personnalisé (optionnel)",
143
+ placeholder='Ex: "Format: Contrat_[TYPE]_[CLIENT]_[JJMMAAAA]"',
144
+ lines=3,
145
  )
146
+ btn = gr.Button("🚀 Générer le nom", variant="primary")
 
147
 
148
  with gr.Column():
149
+ preview = gr.Image(label="👁️ Aperçu Page 1", type="pil")
150
+ output = gr.Textbox(label="📝 Nom de fichier généré", lines=2)
151
 
 
152
  btn.click(
153
  fn=generate_filename,
154
+ inputs=[file_input, prompt_input],
155
+ outputs=[preview, output],
156
+ )
157
+
158
+ gr.Markdown(
159
+ "---\n"
160
+ "ℹ️ *Modèle : `google/gemma-4-E4B-it` · "
161
+ "La clé `HF_TOKEN` doit être définie dans les Secrets du Space.*"
162
  )
163
 
 
164
  demo.launch(show_error=True)