Spaces:
Sleeping
Sleeping
import gradio as gr | |
import difflib | |
from docx import Document | |
import json | |
import os | |
import html # AJOUT CRUCIAL pour échapper le HTML | |
def extract_text_from_file(file_path): | |
"""Extrait le texte d'un fichier selon son extension""" | |
if not file_path: | |
return "" | |
file_extension = os.path.splitext(file_path)[1].lower() | |
try: | |
if file_extension in ['.txt', '.py', '.md', '.html', '.xhtml']: | |
with open(file_path, 'r', encoding='utf-8') as f: | |
return f.read() | |
elif file_extension == '.json': | |
with open(file_path, 'r', encoding='utf-8') as f: | |
data = json.load(f) | |
return json.dumps(data, indent=2, ensure_ascii=False) | |
elif file_extension in ['.docx', '.doc']: | |
doc = Document(file_path) | |
return '\n'.join([paragraph.text for paragraph in doc.paragraphs]) | |
else: | |
return "Format de fichier non supporté" | |
except Exception as e: | |
return f"Erreur lors de la lecture du fichier: {str(e)}" | |
def compare_files(file1, file2): | |
"""Compare deux fichiers avec ÉCHAPPEMENT HTML pour affichage complet""" | |
if not file1 or not file2: | |
return None, "<p style='color: #ff3b30; text-align: center; padding: 20px;'>Veuillez sélectionner deux fichiers à comparer</p>" | |
path1 = file1.name if hasattr(file1, 'name') else file1 | |
path2 = file2.name if hasattr(file2, 'name') else file2 | |
text1 = extract_text_from_file(path1) | |
text2 = extract_text_from_file(path2) | |
lines1 = text1.splitlines() | |
lines2 = text2.splitlines() | |
# Utiliser SequenceMatcher avec autojunk=False pour éviter les heuristiques | |
matcher = difflib.SequenceMatcher(None, lines1, lines2, autojunk=False) | |
html_result = [] | |
text_result = [] | |
for tag, i1, i2, j1, j2 in matcher.get_opcodes(): | |
if tag == 'equal': | |
# Lignes identiques - SANS COULEUR | |
for raw_line in lines2[j1:j2]: | |
# ÉCHAPPEMENT HTML CRUCIAL | |
escaped_line = html.escape(raw_line).replace(' ', ' ') or ' ' | |
html_result.append(f"<div style='padding: 4px 12px; line-height: 1.8; border-left: 3px solid transparent; word-wrap: break-word; white-space: pre-wrap;'>{escaped_line}</div>") | |
text_result.append(f" {raw_line}") | |
elif tag == 'delete': | |
# Lignes supprimées du fichier 1 - ROUGE | |
for raw_line in lines1[i1:i2]: | |
escaped_line = html.escape(raw_line).replace(' ', ' ') | |
html_result.append(f"<div style='background-color: #ffebee; color: #c62828; padding: 4px 12px; line-height: 1.8; border-left: 3px solid #f44336; margin: 1px 0; word-wrap: break-word; white-space: pre-wrap;'><strong>-</strong> {escaped_line}</div>") | |
text_result.append(f"- {raw_line}") | |
elif tag == 'insert': | |
# Lignes ajoutées dans le fichier 2 - VERT | |
for raw_line in lines2[j1:j2]: | |
escaped_line = html.escape(raw_line).replace(' ', ' ') | |
html_result.append(f"<div style='background-color: #e8f5e8; color: #2e7d32; padding: 4px 12px; line-height: 1.8; border-left: 3px solid #4caf50; margin: 1px 0; word-wrap: break-word; white-space: pre-wrap;'><strong>+</strong> {escaped_line}</div>") | |
text_result.append(f"+ {raw_line}") | |
elif tag == 'replace': | |
# Lignes remplacées - D'abord les anciennes (ROUGE), puis les nouvelles (VERT) | |
for raw_line in lines1[i1:i2]: | |
escaped_line = html.escape(raw_line).replace(' ', ' ') | |
html_result.append(f"<div style='background-color: #ffebee; color: #c62828; padding: 4px 12px; line-height: 1.8; border-left: 3px solid #f44336; margin: 1px 0; word-wrap: break-word; white-space: pre-wrap;'><strong>-</strong> {escaped_line}</div>") | |
text_result.append(f"- {raw_line}") | |
for raw_line in lines2[j1:j2]: | |
escaped_line = html.escape(raw_line).replace(' ', ' ') | |
html_result.append(f"<div style='background-color: #e8f5e8; color: #2e7d32; padding: 4px 12px; line-height: 1.8; border-left: 3px solid #4caf50; margin: 1px 0; word-wrap: break-word; white-space: pre-wrap;'><strong>+</strong> {escaped_line}</div>") | |
text_result.append(f"+ {raw_line}") | |
# Créer le fichier téléchargeable | |
download_content = '\n'.join(text_result) | |
filename1_base = os.path.splitext(os.path.basename(path1))[0] | |
filename2_base = os.path.splitext(os.path.basename(path2))[0] | |
download_filename = f"comparaison_{filename1_base}_{filename2_base}.txt" | |
with open(download_filename, 'w', encoding='utf-8') as f: | |
f.write(f"COMPARAISON DE FICHIERS\n") | |
f.write(f"Référence: {os.path.basename(path1)}\n") | |
f.write(f"Comparé: {os.path.basename(path2)}\n") | |
f.write(f"{'='*50}\n") | |
f.write(f"LÉGENDE:\n") | |
f.write(f"- : Ligne supprimée du fichier de référence\n") | |
f.write(f"+ : Ligne ajoutée/modifiée dans le second fichier\n") | |
f.write(f" (avec deux espaces) : Ligne inchangée\n") | |
f.write(f"{'='*50}\n\n") | |
f.write(download_content) | |
# HTML pour l'affichage avec conteneur défilant | |
result_html = f""" | |
<div style='font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; | |
background: #ffffff; border-radius: 12px; padding: 20px; | |
border: 1px solid #e5e5e7;'> | |
<div style='display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid #e5e5e7;'> | |
<h3 style='color: #1d1d1f; margin: 0; font-size: 18px; font-weight: 600;'> | |
📊 COMPARAISON COMPLÈTE | |
</h3> | |
<div style='font-size: 12px; color: #8e8e93;'> | |
<span style='color: #2e7d32;'>+ Ajouts/Modifications</span> | <span style='color: #c62828;'>- Suppressions</span> | | |
<span style='color: #1d1d1f;'>Sans couleur: Inchangé</span> | |
</div> | |
</div> | |
<div style='max-height: 70vh; overflow-y: auto; font-family: "SF Mono", Monaco, "Cascadia Code", monospace; font-size: 14px; | |
max-width: 100%; overflow-wrap: break-word; word-break: break-word;'> | |
{''.join(html_result)} | |
</div> | |
<div style='margin-top: 16px; padding-top: 12px; border-top: 1px solid #e5e5e7; font-size: 12px; color: #8e8e93; text-align: center;'> | |
✅ Affichage complet garanti avec échappement HTML - {len(html_result)} lignes affichées | |
</div> | |
</div> | |
""" | |
return download_filename, result_html | |
def clear_files(): | |
"""Efface les fichiers et les résultats""" | |
return None, None, None, "" | |
def load_example(): | |
"""Charge les fichiers d'exemple""" | |
reference_content = """Les bienfaits des salades de fruits sur la santé | |
Les salades de fruits sont une excellente source de vitamines et minéraux essentiels. | |
Elles contiennent de la vitamine C qui renforce le système immunitaire. | |
Les fibres présentes dans les fruits favorisent une bonne digestion. | |
Les antioxydants protègent les cellules contre le vieillissement. | |
Une consommation régulière peut réduire les risques de maladies cardiovasculaires. | |
Les fruits apportent des sucres naturels qui donnent de l'énergie. | |
Il est recommandé de consommer 5 portions de fruits et légumes par jour. | |
Les salades de fruits sont rafraîchissantes et hydratantes. | |
Elles constituent un dessert sain et peu calorique. | |
En conclusion, intégrer des salades de fruits dans son alimentation est bénéfique.""" | |
updated_content = """Les bienfaits des salades de fruits sur la santé | |
Les salades de fruits sont une excellente source de vitamines, minéraux et antioxydants essentiels. | |
Elles contiennent de la vitamine C et de la vitamine A qui renforcent le système immunitaire. | |
Les fibres présentes dans les fruits favorisent une bonne digestion et régulent le transit intestinal. | |
Les antioxydants naturels protègent les cellules contre le vieillissement prématuré et les radicaux libres. | |
Une consommation régulière peut réduire significativement les risques de maladies cardiovasculaires et de diabète. | |
Les fruits apportent des sucres naturels qui donnent de l'énergie sans provoquer de pic glycémique. | |
Il est fortement recommandé de consommer au minimum 5 portions de fruits et légumes par jour selon l'OMS. | |
Les salades de fruits sont rafraîchissantes, hydratantes et parfaites pour l'été. | |
Elles constituent un dessert sain, peu calorique et riche en nutriments essentiels. | |
IMPORTANT: Privilégier les fruits de saison et biologiques pour maximiser les bienfaits nutritionnels. | |
En conclusion, intégrer quotidiennement des salades de fruits dans son alimentation est extrêmement bénéfique pour la santé.""" | |
with open('reference_sante.txt', 'w', encoding='utf-8') as f: | |
f.write(reference_content) | |
with open('version_amelioree.txt', 'w', encoding='utf-8') as f: | |
f.write(updated_content) | |
return 'reference_sante.txt', 'version_amelioree.txt' | |
# CSS CORRIGÉ avec sélecteurs robustes pour zones d'upload BLANCHES | |
custom_css = """ | |
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
:root { | |
--apple-radius: 12px; | |
} | |
.gradio-container { | |
font-family: -apple-system, BlinkMacSystemFont, 'Inter', 'Segoe UI', Roboto, sans-serif !important; | |
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%) !important; | |
} | |
.header-container { | |
background: linear-gradient(135deg, #1d1d1f 0%, #2d2d2f 100%) !important; | |
color: white !important; | |
padding: 24px !important; | |
margin: -8px -8px 24px -8px !important; | |
border-radius: 0 0 var(--apple-radius) var(--apple-radius) !important; | |
box-shadow: 0 4px 20px rgba(0,0,0,0.1) !important; | |
} | |
.header-title { | |
font-size: 32px !important; | |
font-weight: 700 !important; | |
margin: 0 !important; | |
text-align: center !important; | |
background: linear-gradient(45deg, #ffffff, #e0e0e0) !important; | |
-webkit-background-clip: text !important; | |
-webkit-text-fill-color: transparent !important; | |
background-clip: text !important; | |
} | |
.description-container { | |
background: rgba(255,255,255,0.9) !important; | |
backdrop-filter: blur(10px) !important; | |
border-radius: var(--apple-radius) !important; | |
padding: 20px !important; | |
margin-bottom: 24px !important; | |
border: 1px solid rgba(255,255,255,0.2) !important; | |
box-shadow: 0 8px 32px rgba(0,0,0,0.1) !important; | |
} | |
/* TOUS LES BOUTONS EN NOIR */ | |
.gr-button, | |
.gr-button[data-variant="primary"], | |
.gr-button[data-variant="secondary"], | |
button { | |
background: #1d1d1f !important; | |
border: none !important; | |
border-radius: var(--apple-radius) !important; | |
color: white !important; | |
font-weight: 600 !important; | |
padding: 12px 24px !important; | |
transition: all 0.3s ease !important; | |
box-shadow: 0 4px 16px rgba(0,0,0,0.2) !important; | |
} | |
.gr-button:hover, | |
.gr-button[data-variant="primary"]:hover, | |
.gr-button[data-variant="secondary"]:hover, | |
button:hover { | |
transform: translateY(-1px) !important; | |
box-shadow: 0 6px 20px rgba(0,0,0,0.3) !important; | |
background: #2d2d2f !important; | |
color: white !important; | |
} | |
/* ZONES D'UPLOAD BLANCHES - SÉLECTEUR ROBUSTE */ | |
div[data-testid="file-upload"], | |
div[data-testid="file-upload"] > div, | |
.gr-file-upload, | |
.gr-file-upload > div, | |
.gradio-file, | |
.gradio-file > div { | |
background: #ffffff !important; | |
background-color: #ffffff !important; | |
border: 2px dashed #d1d5db !important; | |
border-radius: var(--apple-radius) !important; | |
transition: all 0.3s ease !important; | |
} | |
div[data-testid="file-upload"]:hover, | |
div[data-testid="file-upload"] > div:hover, | |
.gr-file-upload:hover, | |
.gr-file-upload > div:hover { | |
border-color: #9ca3af !important; | |
background: #f9fafb !important; | |
background-color: #f9fafb !important; | |
} | |
/* Forcer le blanc sur tous les éléments enfants */ | |
div[data-testid="file-upload"] *, | |
.gr-file-upload *, | |
.gradio-file * { | |
background-color: transparent !important; | |
} | |
/* CORRECTION DE LA MISE EN PAGE FLEXBOX DE GRADIO */ | |
.gradio-container > .main, | |
.gradio-container > .main > .wrap, | |
.gradio-container > .main > .wrap > .contain { | |
display: block !important; | |
} | |
/* Responsive */ | |
@media (max-width: 768px) { | |
.header-title { | |
font-size: 24px !important; | |
} | |
.description-container { | |
padding: 16px !important; | |
} | |
} | |
""" | |
# Interface Gradio | |
with gr.Blocks(css=custom_css, title="Comparateur de Fichiers") as app: | |
# Header | |
gr.HTML(""" | |
<div class="header-container"> | |
<h1 class="header-title">📊 Comparateur de Fichiers</h1> | |
</div> | |
""") | |
# Description | |
gr.HTML(""" | |
<div class="description-container"> | |
<h2 style="color: #1d1d1f; font-size: 20px; font-weight: 600; margin-top: 0;"> | |
🎯 Objectif de l'application | |
</h2> | |
<p style="color: #424245; font-size: 16px; line-height: 1.6; margin-bottom: 16px;"> | |
Cette application compare deux fichiers et affiche toutes les modifications avec un code couleur précis. | |
<strong>Problème de texte coupé RÉSOLU</strong> grâce à l'échappement HTML. | |
</p> | |
<h3 style="color: #1d1d1f; font-size: 18px; font-weight: 600; margin-bottom: 8px;"> | |
📋 Formats supportés | |
</h3> | |
<ul style="color: #424245; font-size: 14px; line-height: 1.6; margin-bottom: 16px;"> | |
<li><strong>Texte :</strong> .txt, .py, .md (Markdown)</li> | |
<li><strong>Web :</strong> .html, .xhtml</li> | |
<li><strong>Données :</strong> .json</li> | |
<li><strong>Documents :</strong> .docx, .doc (Word)</li> | |
</ul> | |
<h3 style="color: #1d1d1f; font-size: 18px; font-weight: 600; margin-bottom: 8px;"> | |
🚀 Comment utiliser | |
</h3> | |
<ol style="color: #424245; font-size: 14px; line-height: 1.6;"> | |
<li>Téléchargez le fichier de <strong>référence</strong> et le fichier à <strong>comparer</strong></li> | |
<li>Cliquez sur "Comparer les fichiers"</li> | |
<li>Téléchargez le fichier résultat puis visualisez : <span style="color: #c62828; font-weight: 600;">- lignes supprimées (rouge)</span>, | |
<span style="color: #2e7d32; font-weight: 600;">+ lignes ajoutées/modifiées (vert)</span>, texte normal (inchangé)</li> | |
</ol> | |
</div> | |
""") | |
# Interface de téléchargement | |
with gr.Row(): | |
with gr.Column(scale=1): | |
file1 = gr.File( | |
label="📁 Fichier de référence", | |
file_types=[".txt", ".py", ".json", ".docx", ".doc", ".md", ".html", ".xhtml"] | |
) | |
with gr.Column(scale=1): | |
file2 = gr.File( | |
label="📁 Fichier à comparer", | |
file_types=[".txt", ".py", ".json", ".docx", ".doc", ".md", ".html", ".xhtml"] | |
) | |
# Boutons | |
with gr.Row(): | |
example_btn = gr.Button("🎯 Charger l'exemple") | |
clear_btn = gr.Button("🗑️ Vider") | |
compare_btn = gr.Button("🔍 Comparer les fichiers", variant="primary") | |
# Fichier téléchargeable et résultat (sans conteneurs Row/Column superflus) | |
download_file = gr.File(label="📥 Télécharger le fichier de comparaison", visible=False) | |
result = gr.HTML(label="Comparaison complète avec affichage garanti") | |
# Événements | |
compare_btn.click( | |
fn=compare_files, | |
inputs=[file1, file2], | |
outputs=[download_file, result] | |
).then( | |
lambda: gr.update(visible=True), | |
inputs=None, | |
outputs=[download_file] | |
) | |
clear_btn.click( | |
fn=clear_files, | |
inputs=[], | |
outputs=[file1, file2, download_file, result] | |
).then( | |
lambda: gr.update(visible=False), | |
inputs=None, | |
outputs=[download_file] | |
) | |
example_btn.click( | |
fn=load_example, | |
inputs=[], | |
outputs=[file1, file2] | |
) | |
# Footer | |
gr.HTML(""" | |
<div style="text-align: center; padding: 20px; color: #8e8e93; font-size: 14px; margin-top: 40px;"> | |
<p>Développé avec ❤️ • Inspiré du design Apple • Problème de texte coupé RÉSOLU ✅</p> | |
</div> | |
""") | |
# Lancement universel | |
if __name__ == "__main__": | |
if os.getenv("SPACE_ID"): | |
print("🚀 Lancement sur Hugging Face Spaces") | |
app.launch(show_error=True) | |
else: | |
print("🚀 Lancement sur Google Colab/Local") | |
app.launch(share=True, show_error=True) | |
print("✅ PROBLÈME DE TEXTE COUPÉ RÉSOLU!") | |
print("🔧 Échappement HTML avec html.escape() ajouté") | |
print("📜 Affichage complet garanti même avec balises HTML/XML") | |
print("⚪ Zones d'upload BLANCHES avec sélecteurs robustes") | |
print("📊 Conteneur défilant pour gros fichiers") | |
print("🚀 Prêt pour tous types de fichiers!") |