File size: 13,236 Bytes
2a7e73b
 
 
e539ebe
 
 
da7151a
e539ebe
6a9bf67
76bc209
 
 
c66292a
 
2a7e73b
 
 
 
e539ebe
 
 
da7151a
e539ebe
 
c66292a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f9bb48d
da7151a
fd19ee7
f9bb48d
2ee29e2
da7151a
2ee29e2
6a9bf67
f9bb48d
6a9bf67
f9bb48d
 
 
 
6a9bf67
f9bb48d
 
 
6a9bf67
f9bb48d
2ee29e2
6a9bf67
f9bb48d
 
 
da7151a
f9bb48d
da7151a
 
f9bb48d
da7151a
f9bb48d
fd19ee7
 
f9bb48d
 
 
 
6a9bf67
f9bb48d
da7151a
 
 
c66292a
6a9bf67
da7151a
e539ebe
fd19ee7
 
6a9bf67
fd19ee7
 
 
 
 
6a9bf67
fd19ee7
 
 
 
 
 
 
 
 
 
 
6a9bf67
fd19ee7
 
6a9bf67
fd19ee7
 
f9bb48d
c66292a
 
e539ebe
c66292a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da7151a
c66292a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76bc209
c66292a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76bc209
c66292a
 
76bc209
c66292a
 
 
 
76bc209
c66292a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
fd19ee7
c66292a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e539ebe
2a7e73b
e539ebe
2a7e73b
 
 
c66292a
 
 
 
 
e539ebe
c66292a
 
 
 
 
2a7e73b
c66292a
 
 
 
 
 
 
 
 
 
 
 
2a7e73b
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
import gradio as gr
from groq import Groq
import os
import base64
from io import BytesIO
import pandas as pd
import re
import tempfile
import unicodedata
from openpyxl import Workbook
from openpyxl.drawing.image import Image as OpenpyxlImage
from openpyxl.styles import Font, Alignment
from PIL import Image
import fitz  # PyMuPDF pour gérer les PDF

# Initialize Groq client
client = Groq(api_key=os.getenv("GROQ_API_KEY"))

def image_to_base64(image):
    """Convert PIL image to base64 string for Groq API."""
    buffered = BytesIO()
    image.save(buffered, format="JPEG")
    return base64.b64encode(buffered.getvalue()).decode("utf-8")

def pdf_to_images(pdf_path):
    """Convert PDF pages to PIL images."""
    images = []
    pdf_document = fitz.open(pdf_path)
    for page_num in range(len(pdf_document)):
        page = pdf_document[page_num]
        # Augmenter la résolution pour une meilleure qualité
        mat = fitz.Matrix(2.0, 2.0)  # zoom x2
        pix = page.get_pixmap(matrix=mat)
        img_data = pix.tobytes("jpeg")
        img = Image.open(BytesIO(img_data))
        images.append(img)
    pdf_document.close()
    return images

def parse_markdown_table_to_df(table_text):
    """Parse un tableau Markdown en Pandas DataFrame de manière robuste."""
    # Nettoyer les <br> en \n pour les sauts de ligne dans les cellules
    table_text = re.sub(r'<br>', '\n', table_text)

    lines = table_text.split('\n')

    # Ignorer les lignes vides, mais garder toutes les lignes non-séparateurs
    data_lines = []
    separator_pattern = r'\|[-| :]+\|'
    for line in lines:
        stripped = line.strip()
        if not stripped:
            continue
        if re.match(separator_pattern, stripped):
            continue
        data_lines.append(stripped)

    if len(data_lines) < 1:
        return pd.DataFrame({"Erreur": ["Aucun tableau Markdown trouvé dans la réponse"]})

    # Extraire les en-têtes (première ligne non vide)
    header_line = data_lines[0]
    headers = [h.strip() for h in header_line.split('|')[1:-1]]
    num_columns = len(headers)

    if num_columns == 0:
        return pd.DataFrame({"Erreur": ["Aucun en-tête valide trouvé"]})

    # Extraire les lignes de données (lignes suivantes)
    rows = []
    for line in data_lines[1:]:
        cells = line.split('|')[1:-1]
        cleaned_cells = [cell.strip() for cell in cells]
        # Gérer le mismatch de colonnes
        if len(cleaned_cells) < num_columns:
            cleaned_cells.extend([''] * (num_columns - len(cleaned_cells)))
        elif len(cleaned_cells) > num_columns:
            cleaned_cells = cleaned_cells[:num_columns]
        rows.append(cleaned_cells)

    # Créer le DataFrame
    df = pd.DataFrame(rows, columns=headers)
    # Filtrer les lignes entièrement vides
    df = df.loc[df.apply(lambda row: any(cell.strip() != '' for cell in row), axis=1)]
    return df if not df.empty else pd.DataFrame({"Erreur": ["Aucune donnée valide extraite"]})

def extract_filename_additional_and_table(response):
    """Extraire le nom de fichier, le texte additionnel et le tableau Markdown de la réponse structurée."""
    filename = "tableau_extrait"
    additional_text = ""
    table_text = ""

    if 'Filename:' in response:
        parts = response.split('Filename:', 1)[1].split('\n', 1)
        filename = parts[0].strip().replace('.xlsx', '')
        remaining = parts[1] if len(parts) > 1 else ""
    else:
        remaining = response

    if 'Additional text:' in remaining:
        parts = remaining.split('Additional text:', 1)[1].split('Table:', 1)
        additional_text = parts[0].strip()
        table_text = parts[1].strip() if len(parts) > 1 else ""
    else:
        table_text = remaining.strip()

    # Sanitizer le nom de fichier
    filename = ''.join(c for c in unicodedata.normalize('NFD', filename) if unicodedata.category(c) != 'Mn')
    filename = re.sub(r'[^a-zA-Z0-9_-]', '_', filename)
    filename = filename[:50]

    return filename, additional_text, table_text

def process_single_image(image, prompt):
    """Process a single image with the Groq API."""
    base64_image = image_to_base64(image)
    
    completion = client.chat.completions.create(
        model="meta-llama/llama-4-scout-17b-16e-instruct",
        messages=[
            {
                "role": "user",
                "content": [
                    {"type": "text", "text": prompt},
                    {
                        "type": "image_url",
                        "image_url": {"url": f"data:image/jpeg;base64,{base64_image}"}
                    }
                ]
            }
        ],
        temperature=0.2,
        max_completion_tokens=4096,
        top_p=1,
        stream=False,
        stop=None
    )
    
    return completion.choices[0].message.content.strip()

def create_excel_file(filename, df, additional_text, images):
    """Create Excel file with table, additional info, and original images."""
    excel_path = f"/tmp/{filename if filename else 'tableau_extrait'}.xlsx"
    wb = Workbook()

    # Feuille pour le tableau
    ws_table = wb.active
    ws_table.title = 'Tableau_Extrait'
    if not df.empty and "Erreur" not in df.columns:
        # Écrire les en-têtes en gras
        for col_num, value in enumerate(df.columns.values, start=1):
            cell = ws_table.cell(row=1, column=col_num, value=value)
            cell.font = Font(bold=True)
            cell.alignment = Alignment(wrap_text=True, vertical='top')
            ws_table.column_dimensions[chr(64 + col_num)].width = max(len(str(value)) * 1.2, 15)

        # Écrire les données avec wrap text
        for row_num, row in enumerate(df.values, start=2):
            for col_num, value in enumerate(row, start=1):
                cell = ws_table.cell(row=row_num, column=col_num, value=value)
                cell.alignment = Alignment(wrap_text=True, vertical='top')
                ws_table.row_dimensions[row_num].height = max(len(str(value).split('\n')) * 15, 20)
    else:
        ws_table.cell(row=1, column=1, value="Erreur lors du parsing du tableau")
        ws_table.column_dimensions['A'].width = 50
        ws_table.row_dimensions[1].height = 20

    # Feuille pour le texte additionnel
    if additional_text:
        ws_additional = wb.create_sheet(title='Infos_Supplementaires')
        lines = additional_text.split('\n')
        for row_num, line in enumerate(lines, start=1):
            cell = ws_additional.cell(row=row_num, column=1, value=line)
            cell.alignment = Alignment(wrap_text=True, vertical='top')
            ws_additional.column_dimensions['A'].width = max(len(line) * 1.2, 50)
            ws_additional.row_dimensions[row_num].height = max(len(line.split('\n')) * 15, 20)

    # Feuilles pour les images originales
    for idx, img in enumerate(images, start=1):
        img_path = tempfile.NamedTemporaryFile(delete=False, suffix=".jpg").name
        img.save(img_path, format="JPEG")
        
        sheet_name = f'Image_Page_{idx}' if len(images) > 1 else 'Image_Originale'
        ws_image = wb.create_sheet(title=sheet_name)
        excel_img = OpenpyxlImage(img_path)
        ws_image.add_image(excel_img, 'A1')
        
        os.unlink(img_path)

    wb.save(excel_path)
    return excel_path

def process_file(file):
    """Process uploaded file (image or PDF) and generate Excel."""
    if file is None:
        return "Veuillez uploader un fichier (image ou PDF).", None

    try:
        file_path = file.name if hasattr(file, 'name') else file
        file_extension = os.path.splitext(file_path)[1].lower()
        
        # Déterminer si c'est un PDF ou une image
        images = []
        if file_extension == '.pdf':
            images = pdf_to_images(file_path)
            if not images:
                return "Erreur : Le PDF ne contient aucune page.", None
        else:
            # C'est une image
            if isinstance(file, str):
                images = [Image.open(file)]
            else:
                images = [file]

        # Prompt optimisé
        prompt = (
            "Analyse l'image et extrait tout le contenu avec précision. "
            "D'abord, suggère un nom de fichier descriptif et précis pour l'Excel basé sur le contenu principal de l'image "
            "(inclue des éléments clés comme le mois, l'année, ou des identifiants uniques, ex: 'Registre_Heures_Juin_2016'). "
            "Ensuite, extrait TOUT texte additionnel autour, au-dessus, en-dessous ou à côté du tableau "
            "(titres, en-têtes de page, notes, pieds de page, logos, etc.), en le recopiant mot pour mot, même si c'est dispersé. "
            "Si aucun texte additionnel, laisse vide. "
            "Enfin, extrait le tableau COMPLET en entier, en recopiant TOUTES les lignes et colonnes à l'identique, "
            "y compris les lignes vides ou partielles si elles existent dans l'image. "
            "Les en-têtes du tableau (première ligne avec les titres des colonnes) doivent TOUJOURS être inclus dans la section Table, "
            "JAMAIS dans Additional text. "
            "Utilise un format Markdown pour le tableau avec des | pour les colonnes et une ligne |---|---| pour les séparateurs. "
            "Assure-toi que CHAQUE ligne (en-têtes, séparateurs, données) a EXACTEMENT le même nombre de colonnes. "
            "Pour les sauts de ligne dans une cellule, utilise \\n au lieu de <br>. "
            "Remplis les cellules vides avec '' si nécessaire pour maintenir l'alignement. "
            "N'ajoute aucun texte explicatif dans le tableau. "
            "Structure ta réponse exactement comme suit :\n"
            "Filename: [nom_suggéré_précis]\n"
            "Additional text: [tout le texte additionnel, séparé par \\n si plusieurs lignes ; sinon vide]\n"
            "Table:\n"
            "[le tableau Markdown ici]"
        )

        # Traiter chaque page/image
        all_responses = []
        all_tables = []
        all_additional_texts = []
        filename = "tableau_extrait"
        
        for idx, img in enumerate(images):
            response = process_single_image(img, prompt)
            all_responses.append(f"--- Page {idx + 1} ---\n{response}")
            
            # Extraire les informations
            current_filename, additional_text, table_text = extract_filename_additional_and_table(response)
            
            # Utiliser le nom de fichier de la première page
            if idx == 0 and current_filename:
                filename = current_filename
            
            if additional_text:
                all_additional_texts.append(f"Page {idx + 1}:\n{additional_text}")
            
            # Parser le tableau
            df = parse_markdown_table_to_df(table_text)
            all_tables.append((idx + 1, df))

        # Combiner tous les tableaux en un seul DataFrame
        combined_df = pd.DataFrame()
        for page_num, df in all_tables:
            if not df.empty and "Erreur" not in df.columns:
                # Ajouter une colonne "Page" si plusieurs pages
                if len(all_tables) > 1:
                    df.insert(0, 'Page', page_num)
                combined_df = pd.concat([combined_df, df], ignore_index=True)

        # Si aucun tableau valide n'a été trouvé, utiliser le premier DataFrame d'erreur
        if combined_df.empty:
            combined_df = all_tables[0][1] if all_tables else pd.DataFrame({"Erreur": ["Aucune donnée extraite"]})

        # Combiner tous les textes additionnels
        combined_additional_text = "\n\n".join(all_additional_texts) if all_additional_texts else ""

        # Créer le fichier Excel
        excel_path = create_excel_file(filename, combined_df, combined_additional_text, images)

        # Créer la réponse complète
        full_response = "\n\n".join(all_responses) if len(images) > 1 else all_responses[0]

        return full_response, excel_path

    except Exception as e:
        return f"Erreur : {str(e)}", None

# Define Gradio interface
iface = gr.Interface(
    fn=process_file,
    inputs=gr.File(
        label="Uploader une image ou un PDF contenant un tableau",
        file_types=["image", ".pdf"]
    ),
    outputs=[
        gr.Textbox(
            label="Réponse de l'IA (texte additionnel + tableau Markdown)",
            lines=15
        ),
        gr.File(label="Télécharger le fichier Excel (avec tableau, infos supp. et images originales)")
    ],
    title="Extraction de Tableau depuis Image/PDF avec Groq et Export Excel",
    description=(
        "Uploader une image ou un PDF contenant un tableau. "
        "L'IA extrait le texte additionnel et le tableau, puis génère un Excel avec des feuilles séparées, "
        "y compris les images originales. Pour les PDF multi-pages, tous les tableaux sont combinés. "
        "Le résultat n'est pas parfait, veuillez vous relire pour vérifier l'exactitude des réponses. "
        "Les données sont privées et ne sont pas sauvegardées."
    ),
    examples=[
        ["exemple_tableau.jpg"],
        ["exemple_document.pdf"]
    ]
)

# Launch the interface
if __name__ == "__main__":
    iface.launch()