Spaces:
Runtime error
Runtime error
Update app.py
Browse files
app.py
CHANGED
|
@@ -6,7 +6,7 @@ from io import BytesIO
|
|
| 6 |
import pandas as pd
|
| 7 |
import re
|
| 8 |
import tempfile
|
| 9 |
-
import unicodedata
|
| 10 |
from openpyxl import Workbook
|
| 11 |
from openpyxl.drawing.image import Image as OpenpyxlImage
|
| 12 |
from openpyxl.styles import Font, Alignment
|
|
@@ -27,22 +27,21 @@ def parse_markdown_table_to_df(table_text):
|
|
| 27 |
|
| 28 |
lines = table_text.split('\n')
|
| 29 |
|
| 30 |
-
# Ignorer les lignes vides
|
| 31 |
data_lines = []
|
| 32 |
-
|
| 33 |
for line in lines:
|
| 34 |
stripped = line.strip()
|
| 35 |
if not stripped:
|
| 36 |
continue
|
| 37 |
-
if re.match(
|
| 38 |
-
separator_found = True
|
| 39 |
continue
|
| 40 |
data_lines.append(stripped)
|
| 41 |
|
| 42 |
-
if
|
| 43 |
return pd.DataFrame({"Erreur": ["Aucun tableau Markdown trouvé dans la réponse"]})
|
| 44 |
|
| 45 |
-
# Extraire les en-têtes (première ligne)
|
| 46 |
header_line = data_lines[0]
|
| 47 |
headers = [h.strip() for h in header_line.split('|')[1:-1]]
|
| 48 |
num_columns = len(headers)
|
|
@@ -59,25 +58,24 @@ def parse_markdown_table_to_df(table_text):
|
|
| 59 |
if len(cleaned_cells) < num_columns:
|
| 60 |
cleaned_cells.extend([''] * (num_columns - len(cleaned_cells)))
|
| 61 |
elif len(cleaned_cells) > num_columns:
|
| 62 |
-
cleaned_cells = cleaned_cells[:num_columns]
|
| 63 |
rows.append(cleaned_cells)
|
| 64 |
|
| 65 |
-
# Filtrer les lignes entièrement vides (tous '' ou vides)
|
| 66 |
-
rows = [row for row in rows if any(cell.strip() != '' for cell in row)]
|
| 67 |
-
|
| 68 |
# Créer le DataFrame
|
| 69 |
df = pd.DataFrame(rows, columns=headers)
|
|
|
|
|
|
|
| 70 |
return df if not df.empty else pd.DataFrame({"Erreur": ["Aucune donnée valide extraite"]})
|
| 71 |
|
| 72 |
def extract_filename_additional_and_table(response):
|
| 73 |
"""Extraire le nom de fichier, le texte additionnel et le tableau Markdown de la réponse structurée."""
|
| 74 |
-
filename = "tableau_extrait"
|
| 75 |
additional_text = ""
|
| 76 |
table_text = ""
|
| 77 |
|
| 78 |
if 'Filename:' in response:
|
| 79 |
parts = response.split('Filename:', 1)[1].split('\n', 1)
|
| 80 |
-
filename = parts[0].strip().replace('.xlsx', '')
|
| 81 |
remaining = parts[1] if len(parts) > 1 else ""
|
| 82 |
else:
|
| 83 |
remaining = response
|
|
@@ -89,10 +87,10 @@ def extract_filename_additional_and_table(response):
|
|
| 89 |
else:
|
| 90 |
table_text = remaining.strip()
|
| 91 |
|
| 92 |
-
# Sanitizer le nom de fichier
|
| 93 |
filename = ''.join(c for c in unicodedata.normalize('NFD', filename) if unicodedata.category(c) != 'Mn')
|
| 94 |
filename = re.sub(r'[^a-zA-Z0-9_-]', '_', filename)
|
| 95 |
-
filename = filename[:50]
|
| 96 |
|
| 97 |
return filename, additional_text, table_text
|
| 98 |
|
|
@@ -106,15 +104,16 @@ def process_image_and_get_response(image):
|
|
| 106 |
|
| 107 |
# Prompt optimisé pour structure, précision et inclusion des infos supplémentaires
|
| 108 |
prompt = (
|
| 109 |
-
"Analyse l'image et extrait tout le contenu. "
|
| 110 |
-
"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
|
| 111 |
-
"Ensuite, extrait TOUT texte additionnel autour, au-dessus, en-dessous ou à côté du tableau (titres, en-têtes, notes, pieds de page, logos, etc.), en le recopiant mot pour mot, même si c'est dispersé. "
|
| 112 |
"Si aucun texte additionnel, laisse vide. "
|
| 113 |
-
"Enfin, extrait le tableau en entier, en recopiant TOUTES les lignes et colonnes à l'identique, y compris les lignes vides ou partielles si elles existent dans l'image. "
|
|
|
|
| 114 |
"Utilise un format Markdown pour le tableau avec des | pour les colonnes et une ligne |---|---| pour les séparateurs. "
|
| 115 |
-
"Assure-toi que CHAQUE ligne (en-têtes, séparateurs, données) a EXACTEMENT le même nombre de colonnes (compte les | : il doit y en avoir
|
| 116 |
"Pour les sauts de ligne dans une cellule, utilise \n au lieu de <br>. "
|
| 117 |
-
"Remplis les cellules vides avec '' si nécessaire pour maintenir l'alignement
|
| 118 |
"N'ajoute aucun texte explicatif dans le tableau. "
|
| 119 |
"Structure ta réponse exactement comme suit :\n"
|
| 120 |
"Filename: [nom_suggéré_précis]\n"
|
|
@@ -125,7 +124,7 @@ def process_image_and_get_response(image):
|
|
| 125 |
|
| 126 |
try:
|
| 127 |
completion = client.chat.completions.create(
|
| 128 |
-
model="meta-llama/llama-4-
|
| 129 |
messages=[
|
| 130 |
{
|
| 131 |
"role": "user",
|
|
@@ -138,8 +137,8 @@ def process_image_and_get_response(image):
|
|
| 138 |
]
|
| 139 |
}
|
| 140 |
],
|
| 141 |
-
temperature=0.
|
| 142 |
-
max_completion_tokens=
|
| 143 |
top_p=1,
|
| 144 |
stream=False,
|
| 145 |
stop=None
|
|
@@ -170,15 +169,21 @@ def process_image_and_get_response(image):
|
|
| 170 |
cell = ws_table.cell(row=1, column=col_num, value=value)
|
| 171 |
cell.font = Font(bold=True)
|
| 172 |
cell.alignment = Alignment(wrap_text=True, vertical='top')
|
|
|
|
|
|
|
| 173 |
|
| 174 |
# Écrire les données avec wrap text
|
| 175 |
for row_num, row in enumerate(df.values, start=2):
|
| 176 |
for col_num, value in enumerate(row, start=1):
|
| 177 |
cell = ws_table.cell(row=row_num, column=col_num, value=value)
|
| 178 |
cell.alignment = Alignment(wrap_text=True, vertical='top')
|
|
|
|
|
|
|
| 179 |
|
| 180 |
else:
|
| 181 |
ws_table.cell(row=1, column=1, value="Erreur lors du parsing du tableau")
|
|
|
|
|
|
|
| 182 |
|
| 183 |
# Feuille pour le texte additionnel, si présent
|
| 184 |
if additional_text:
|
|
@@ -187,6 +192,8 @@ def process_image_and_get_response(image):
|
|
| 187 |
for row_num, line in enumerate(lines, start=1):
|
| 188 |
cell = ws_additional.cell(row=row_num, column=1, value=line)
|
| 189 |
cell.alignment = Alignment(wrap_text=True, vertical='top')
|
|
|
|
|
|
|
| 190 |
|
| 191 |
# Feuille pour l'image originale
|
| 192 |
ws_image = wb.create_sheet(title='Image_Originale')
|
|
@@ -212,7 +219,7 @@ iface = gr.Interface(
|
|
| 212 |
gr.Textbox(label="Réponse de l'IA (texte additionnel + tableau Markdown pour copier-coller)"),
|
| 213 |
gr.File(label="Télécharger le fichier Excel (avec tableau, infos supp. et image originale)")
|
| 214 |
],
|
| 215 |
-
title="Extraction de Tableau depuis Image
|
| 216 |
description="Uploader une image avec 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 l'image originale. 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."
|
| 217 |
)
|
| 218 |
|
|
|
|
| 6 |
import pandas as pd
|
| 7 |
import re
|
| 8 |
import tempfile
|
| 9 |
+
import unicodedata
|
| 10 |
from openpyxl import Workbook
|
| 11 |
from openpyxl.drawing.image import Image as OpenpyxlImage
|
| 12 |
from openpyxl.styles import Font, Alignment
|
|
|
|
| 27 |
|
| 28 |
lines = table_text.split('\n')
|
| 29 |
|
| 30 |
+
# Ignorer les lignes vides, mais garder toutes les lignes non-séparateurs
|
| 31 |
data_lines = []
|
| 32 |
+
separator_pattern = r'\|[-| :]+\|'
|
| 33 |
for line in lines:
|
| 34 |
stripped = line.strip()
|
| 35 |
if not stripped:
|
| 36 |
continue
|
| 37 |
+
if re.match(separator_pattern, stripped):
|
|
|
|
| 38 |
continue
|
| 39 |
data_lines.append(stripped)
|
| 40 |
|
| 41 |
+
if len(data_lines) < 1:
|
| 42 |
return pd.DataFrame({"Erreur": ["Aucun tableau Markdown trouvé dans la réponse"]})
|
| 43 |
|
| 44 |
+
# Extraire les en-têtes (première ligne non vide)
|
| 45 |
header_line = data_lines[0]
|
| 46 |
headers = [h.strip() for h in header_line.split('|')[1:-1]]
|
| 47 |
num_columns = len(headers)
|
|
|
|
| 58 |
if len(cleaned_cells) < num_columns:
|
| 59 |
cleaned_cells.extend([''] * (num_columns - len(cleaned_cells)))
|
| 60 |
elif len(cleaned_cells) > num_columns:
|
| 61 |
+
cleaned_cells = cleaned_cells[:num_columns]
|
| 62 |
rows.append(cleaned_cells)
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
# Créer le DataFrame
|
| 65 |
df = pd.DataFrame(rows, columns=headers)
|
| 66 |
+
# Filtrer les lignes entièrement vides (optionnel, mais conservé pour éviter les lignes inutiles)
|
| 67 |
+
df = df.loc[df.apply(lambda row: any(cell.strip() != '' for cell in row), axis=1)]
|
| 68 |
return df if not df.empty else pd.DataFrame({"Erreur": ["Aucune donnée valide extraite"]})
|
| 69 |
|
| 70 |
def extract_filename_additional_and_table(response):
|
| 71 |
"""Extraire le nom de fichier, le texte additionnel et le tableau Markdown de la réponse structurée."""
|
| 72 |
+
filename = "tableau_extrait"
|
| 73 |
additional_text = ""
|
| 74 |
table_text = ""
|
| 75 |
|
| 76 |
if 'Filename:' in response:
|
| 77 |
parts = response.split('Filename:', 1)[1].split('\n', 1)
|
| 78 |
+
filename = parts[0].strip().replace('.xlsx', '')
|
| 79 |
remaining = parts[1] if len(parts) > 1 else ""
|
| 80 |
else:
|
| 81 |
remaining = response
|
|
|
|
| 87 |
else:
|
| 88 |
table_text = remaining.strip()
|
| 89 |
|
| 90 |
+
# Sanitizer le nom de fichier
|
| 91 |
filename = ''.join(c for c in unicodedata.normalize('NFD', filename) if unicodedata.category(c) != 'Mn')
|
| 92 |
filename = re.sub(r'[^a-zA-Z0-9_-]', '_', filename)
|
| 93 |
+
filename = filename[:50]
|
| 94 |
|
| 95 |
return filename, additional_text, table_text
|
| 96 |
|
|
|
|
| 104 |
|
| 105 |
# Prompt optimisé pour structure, précision et inclusion des infos supplémentaires
|
| 106 |
prompt = (
|
| 107 |
+
"Analyse l'image et extrait tout le contenu avec précision. "
|
| 108 |
+
"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'). "
|
| 109 |
+
"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é. "
|
| 110 |
"Si aucun texte additionnel, laisse vide. "
|
| 111 |
+
"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. "
|
| 112 |
+
"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. "
|
| 113 |
"Utilise un format Markdown pour le tableau avec des | pour les colonnes et une ligne |---|---| pour les séparateurs. "
|
| 114 |
+
"Assure-toi que CHAQUE ligne (en-têtes, séparateurs, données) a EXACTEMENT le même nombre de colonnes (compte les | : il doit y en avoir 13 pour 12 colonnes, incluant les | de début et fin). "
|
| 115 |
"Pour les sauts de ligne dans une cellule, utilise \n au lieu de <br>. "
|
| 116 |
+
"Remplis les cellules vides avec '' si nécessaire pour maintenir l'alignement. "
|
| 117 |
"N'ajoute aucun texte explicatif dans le tableau. "
|
| 118 |
"Structure ta réponse exactement comme suit :\n"
|
| 119 |
"Filename: [nom_suggéré_précis]\n"
|
|
|
|
| 124 |
|
| 125 |
try:
|
| 126 |
completion = client.chat.completions.create(
|
| 127 |
+
model="meta-llama/llama-4-scout-17b-16e-instruct",
|
| 128 |
messages=[
|
| 129 |
{
|
| 130 |
"role": "user",
|
|
|
|
| 137 |
]
|
| 138 |
}
|
| 139 |
],
|
| 140 |
+
temperature=0.2,
|
| 141 |
+
max_completion_tokens=4096,
|
| 142 |
top_p=1,
|
| 143 |
stream=False,
|
| 144 |
stop=None
|
|
|
|
| 169 |
cell = ws_table.cell(row=1, column=col_num, value=value)
|
| 170 |
cell.font = Font(bold=True)
|
| 171 |
cell.alignment = Alignment(wrap_text=True, vertical='top')
|
| 172 |
+
# Ajuster la largeur des colonnes (auto-ajustement approximatif)
|
| 173 |
+
ws_table.column_dimensions[chr(64 + col_num)].width = max(len(str(value)) * 1.2, 15)
|
| 174 |
|
| 175 |
# Écrire les données avec wrap text
|
| 176 |
for row_num, row in enumerate(df.values, start=2):
|
| 177 |
for col_num, value in enumerate(row, start=1):
|
| 178 |
cell = ws_table.cell(row=row_num, column=col_num, value=value)
|
| 179 |
cell.alignment = Alignment(wrap_text=True, vertical='top')
|
| 180 |
+
# Ajuster la hauteur des lignes
|
| 181 |
+
ws_table.row_dimensions[row_num].height = max(len(str(value).split('\n')) * 15, 20)
|
| 182 |
|
| 183 |
else:
|
| 184 |
ws_table.cell(row=1, column=1, value="Erreur lors du parsing du tableau")
|
| 185 |
+
ws_table.column_dimensions['A'].width = 50
|
| 186 |
+
ws_table.row_dimensions[1].height = 20
|
| 187 |
|
| 188 |
# Feuille pour le texte additionnel, si présent
|
| 189 |
if additional_text:
|
|
|
|
| 192 |
for row_num, line in enumerate(lines, start=1):
|
| 193 |
cell = ws_additional.cell(row=row_num, column=1, value=line)
|
| 194 |
cell.alignment = Alignment(wrap_text=True, vertical='top')
|
| 195 |
+
ws_additional.column_dimensions['A'].width = max(len(line) * 1.2, 50)
|
| 196 |
+
ws_additional.row_dimensions[row_num].height = max(len(line.split('\n')) * 15, 20)
|
| 197 |
|
| 198 |
# Feuille pour l'image originale
|
| 199 |
ws_image = wb.create_sheet(title='Image_Originale')
|
|
|
|
| 219 |
gr.Textbox(label="Réponse de l'IA (texte additionnel + tableau Markdown pour copier-coller)"),
|
| 220 |
gr.File(label="Télécharger le fichier Excel (avec tableau, infos supp. et image originale)")
|
| 221 |
],
|
| 222 |
+
title="Extraction de Tableau depuis Image avec Groq et Export Excel",
|
| 223 |
description="Uploader une image avec 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 l'image originale. 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."
|
| 224 |
)
|
| 225 |
|