devalender commited on
Commit
e15b9b2
·
verified ·
1 Parent(s): 3c429f1

Create app2

Browse files
Files changed (1) hide show
  1. app2 +175 -0
app2 ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import fitz # PyMuPDF
3
+ import base64
4
+ import os
5
+ import re
6
+ from io import BytesIO
7
+ from PIL import Image
8
+ from huggingface_hub import InferenceClient
9
+
10
+ # ─── Config ───────────────────────────────────────────────────────────────────
11
+ MODEL_ID = "google/gemma-4-E4B-it" # multimodal, léger, parfait pour cette tâche
12
+ HF_TOKEN = os.environ.get("HF_TOKEN") # défini en secret dans HF Spaces
13
+
14
+ # ─── Helpers ──────────────────────────────────────────────────────────────────
15
+
16
+ def pdf_first_page_to_base64(pdf_path: str) -> tuple[str, Image.Image]:
17
+ """Extrait la première page du PDF et retourne image base64 + PIL Image."""
18
+ doc = fitz.open(pdf_path)
19
+ page = doc[0]
20
+ # Rendu à 150 DPI — assez pour lire, assez léger pour l'API
21
+ mat = fitz.Matrix(150 / 72, 150 / 72)
22
+ pix = page.get_pixmap(matrix=mat)
23
+ doc.close()
24
+
25
+ img_bytes = pix.tobytes("png")
26
+ b64 = base64.b64encode(img_bytes).decode("utf-8")
27
+ pil_img = Image.open(BytesIO(img_bytes))
28
+ return b64, pil_img
29
+
30
+
31
+ def sanitize_filename(name: str) -> str:
32
+ """Nettoie le nom retourné par le modèle pour en faire un nom de fichier valide."""
33
+ # Garder uniquement lettres, chiffres, tirets, underscores
34
+ name = name.strip().strip('"').strip("'")
35
+ name = re.sub(r"[^\w\s\-]", "", name)
36
+ name = re.sub(r"\s+", "_", name)
37
+ name = name[:80] # max 80 caractères
38
+ return name if name else "document_sans_nom"
39
+
40
+
41
+ # ─── Fonction principale ───────────────────────────────────────────────────────
42
+
43
+ def name_pdf(pdf_file, user_prompt: str, langue: str) -> tuple:
44
+ """
45
+ 1. Convertit la page 1 du PDF en image
46
+ 2. Envoie image + prompt à Gemma 4 E4B via Inference API
47
+ 3. Retourne le nom suggéré
48
+ """
49
+ if pdf_file is None:
50
+ return None, "", "⚠️ Veuillez uploader un fichier PDF."
51
+
52
+ if not HF_TOKEN:
53
+ return None, "", "❌ Token HF manquant. Configurez HF_TOKEN dans les secrets du Space."
54
+
55
+ # Étape 1 — Convertir PDF → image
56
+ try:
57
+ b64_image, pil_preview = pdf_first_page_to_base64(pdf_file.name)
58
+ except Exception as e:
59
+ return None, "", f"❌ Erreur lecture PDF : {e}"
60
+
61
+ # Étape 2 — Construire le prompt
62
+ langue_instruction = {
63
+ "Français": "Réponds uniquement en français.",
64
+ "English": "Reply only in English.",
65
+ "Arabe / عربي": "أجب باللغة العربية فقط.",
66
+ }.get(langue, "")
67
+
68
+ system_prompt = (
69
+ "Tu es un assistant spécialisé dans la gestion documentaire. "
70
+ "Tu analyses la première page d'un document et tu proposes un nom de fichier court, "
71
+ "descriptif et professionnel. "
72
+ "Retourne UNIQUEMENT le nom du fichier, sans extension, sans explication, sans guillemets."
73
+ )
74
+
75
+ user_message = (
76
+ f"{user_prompt}\n\n"
77
+ f"{langue_instruction}\n\n"
78
+ "Propose un nom de fichier court (3 à 6 mots maximum), en snake_case, "
79
+ "basé sur le contenu visible de cette page."
80
+ )
81
+
82
+ # Étape 3 — Appel à l'Inference API (multimodal)
83
+ try:
84
+ client = InferenceClient(token=HF_TOKEN)
85
+
86
+ response = client.chat_completion(
87
+ model=MODEL_ID,
88
+ messages=[
89
+ {"role": "system", "content": system_prompt},
90
+ {
91
+ "role": "user",
92
+ "content": [
93
+ {
94
+ "type": "image_url",
95
+ "image_url": {
96
+ "url": f"data:image/png;base64,{b64_image}"
97
+ },
98
+ },
99
+ {"type": "text", "text": user_message},
100
+ ],
101
+ },
102
+ ],
103
+ max_tokens=60,
104
+ temperature=0.3,
105
+ )
106
+
107
+ raw_name = response.choices[0].message.content.strip()
108
+ clean_name = sanitize_filename(raw_name)
109
+
110
+ except Exception as e:
111
+ return pil_preview, "", f"❌ Erreur API Hugging Face : {e}"
112
+
113
+ return pil_preview, clean_name, f"✅ Nom suggéré : **{clean_name}.pdf**"
114
+
115
+
116
+ # ─── Interface Gradio ──────────────────────────────────────────────────────────
117
+
118
+ DESCRIPTION = """
119
+ # 📄 PDF Auto-Namer
120
+ ### Propulsé par Gemma 4 E4B (multimodal) via Hugging Face Inference API
121
+
122
+ Uploadez un PDF → l'IA analyse la première page → vous obtenez un nom de fichier propre.
123
+ """
124
+
125
+ EXAMPLES = [
126
+ [None, "Lis la première page et donne un nom professionnel à ce document", "Français"],
127
+ [None, "Give this document a short, professional filename", "English"],
128
+ ]
129
+
130
+ with gr.Blocks(theme=gr.themes.Soft(), title="PDF Auto-Namer") as demo:
131
+ gr.Markdown(DESCRIPTION)
132
+
133
+ with gr.Row():
134
+ with gr.Column(scale=1):
135
+ pdf_input = gr.File(
136
+ label="📂 Uploader votre PDF",
137
+ file_types=[".pdf"],
138
+ type="filepath",
139
+ )
140
+ prompt_input = gr.Textbox(
141
+ label="💬 Prompt personnalisé",
142
+ placeholder="Ex: Donne un nom court et professionnel à ce document...",
143
+ value="Analyse la première page et propose un nom de fichier court et descriptif.",
144
+ lines=3,
145
+ )
146
+ langue_input = gr.Radio(
147
+ label="🌍 Langue de sortie",
148
+ choices=["Français", "English", "Arabe / عربي"],
149
+ value="Français",
150
+ )
151
+ submit_btn = gr.Button("🚀 Générer le nom", variant="primary", size="lg")
152
+
153
+ with gr.Column(scale=1):
154
+ preview_img = gr.Image(label="👁️ Aperçu — Page 1 du PDF", type="pil")
155
+ name_output = gr.Textbox(
156
+ label="📝 Nom de fichier suggéré",
157
+ placeholder="Le nom apparaîtra ici...",
158
+ interactive=False,
159
+ )
160
+ status_output = gr.Markdown("")
161
+
162
+ submit_btn.click(
163
+ fn=name_pdf,
164
+ inputs=[pdf_input, prompt_input, langue_input],
165
+ outputs=[preview_img, name_output, status_output],
166
+ )
167
+
168
+ gr.Markdown(
169
+ "---\n"
170
+ "ℹ️ *Ce Space utilise l'[Inference API HF](https://huggingface.co/inference-api) "
171
+ "— aucun modèle n'est chargé localement. Modèle : `google/gemma-4-E4B-it`.*"
172
+ )
173
+
174
+ if __name__ == "__main__":
175
+ demo.launch()