book-generator-orgmode/generate_corrections_page.py

661 lines
No EOL
22 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script pour générer une page web interactive de correction des erreurs orthographiques et grammaticales.
Ce script:
1. Lit le rapport d'erreurs et le fichier CSV des erreurs
2. Génère une page HTML interactive dans le dossier build
3. Permet de corriger les erreurs directement dans le fichier livre.org
4. Permet d'ajouter des mots au dictionnaire personnalisé
5. Permet de marquer des erreurs comme "à ne pas traiter"
"""
import os
import re
import json
import csv
import shutil
from datetime import datetime
# Créer le dossier build s'il n'existe pas
if not os.path.exists('build'):
os.makedirs('build')
# Créer le dossier pour les ressources statiques s'il n'existe pas
STATIC_DIR = os.path.join('build', 'static')
if not os.path.exists(STATIC_DIR):
os.makedirs(STATIC_DIR)
# Créer les sous-dossiers pour CSS et JS
CSS_DIR = os.path.join(STATIC_DIR, 'css')
JS_DIR = os.path.join(STATIC_DIR, 'js')
for directory in [CSS_DIR, JS_DIR]:
if not os.path.exists(directory):
os.makedirs(directory)
def parse_error_report(report_path):
"""
Parse le rapport d'erreurs pour extraire les informations sur les erreurs.
"""
with open(report_path, 'r', encoding='utf-8') as file:
content = file.read()
# Extraire les sections par chapitre
chapter_pattern = r'## Chapitre: (.*?)\n\n(.*?)(?=\n---|\Z)'
chapters = re.findall(chapter_pattern, content, re.DOTALL)
errors_by_chapter = {}
for chapter_title, chapter_content in chapters:
# Extraire les erreurs d'orthographe
spelling_pattern = r'### Erreurs d\'orthographe\n\n(.*?)(?=\n\n### Erreurs grammaticales|\Z)'
spelling_match = re.search(spelling_pattern, chapter_content, re.DOTALL)
spelling_errors = []
if spelling_match and "Aucune erreur d'orthographe détectée" not in spelling_match.group(1):
spelling_text = spelling_match.group(1)
error_pattern = r'- \*\*(.*?)\*\*: (.*?)(?=\n- \*\*|\Z)'
for error_match in re.finditer(error_pattern, spelling_text, re.DOTALL):
word = error_match.group(1)
suggestions_text = error_match.group(2).strip()
suggestions = []
if suggestions_text != "Aucune suggestion":
suggestions = [s.strip() for s in suggestions_text.split(',')]
spelling_errors.append({
'word': word,
'suggestions': suggestions,
'type': 'spelling'
})
# Extraire les erreurs grammaticales
grammar_pattern = r'### Erreurs grammaticales\n\n(.*?)(?=\n\n---|\Z)'
grammar_match = re.search(grammar_pattern, chapter_content, re.DOTALL)
grammar_errors = []
if grammar_match and "Aucune erreur grammaticale détectée" not in grammar_match.group(1):
grammar_text = grammar_match.group(1)
error_pattern = r'- \*\*Erreur\*\*: (.*?)\n - \*\*Contexte\*\*: (.*?)\n - \*\*Suggestions\*\*: (.*?)(?=\n\n- \*\*Erreur|\Z)'
for error_match in re.finditer(error_pattern, grammar_text, re.DOTALL):
message = error_match.group(1).strip()
context = error_match.group(2).strip()
suggestions_text = error_match.group(3).strip()
# Extraire le texte en gras du contexte (l'erreur elle-même)
error_text_match = re.search(r'\*\*(.*?)\*\*', context)
error_text = error_text_match.group(1) if error_text_match else ""
# Nettoyer le contexte pour l'affichage
clean_context = re.sub(r'\*\*(.*?)\*\*', r'\1', context)
suggestions = []
if suggestions_text != "Aucune suggestion":
suggestions = [s.strip() for s in suggestions_text.split(',')]
grammar_errors.append({
'message': message,
'context': clean_context,
'error_text': error_text,
'suggestions': suggestions,
'type': 'grammar'
})
errors_by_chapter[chapter_title] = {
'spelling': spelling_errors,
'grammar': grammar_errors
}
return errors_by_chapter
def create_css():
"""Crée le fichier CSS pour la page de corrections."""
css_content = """
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #f5f5f5;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
background-color: #2c3e50;
color: white;
padding: 20px;
text-align: center;
margin-bottom: 30px;
}
h1, h2, h3 {
margin-top: 0;
}
.chapter-section {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 20px;
margin-bottom: 30px;
}
.error-card {
border: 1px solid #ddd;
border-radius: 5px;
padding: 15px;
margin-bottom: 15px;
background-color: #fff;
}
.error-card.spelling {
border-left: 5px solid #3498db;
}
.error-card.grammar {
border-left: 5px solid #e74c3c;
}
.error-context {
background-color: #f9f9f9;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
font-family: monospace;
white-space: pre-wrap;
line-height: 1.5;
}
.error-word {
font-weight: bold;
color: #e74c3c;
text-decoration: underline;
text-decoration-style: wavy;
text-decoration-color: #e74c3c;
}
.suggestion-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.suggestion-btn {
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.suggestion-btn:hover {
background-color: #2980b9;
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 15px;
}
.action-btn {
background-color: #2ecc71;
color: white;
border: none;
border-radius: 4px;
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.action-btn.ignore {
background-color: #95a5a6;
}
.action-btn.dictionary {
background-color: #f39c12;
}
.action-btn:hover {
opacity: 0.9;
}
.success-message {
display: none;
background-color: #2ecc71;
color: white;
padding: 10px;
border-radius: 5px;
margin-top: 10px;
text-align: center;
}
.error-message {
display: none;
background-color: #e74c3c;
color: white;
padding: 10px;
border-radius: 5px;
margin-top: 10px;
text-align: center;
}
.tabs {
display: flex;
margin-bottom: 20px;
}
.tab {
padding: 10px 20px;
background-color: #ddd;
cursor: pointer;
border-radius: 5px 5px 0 0;
margin-right: 5px;
}
.tab.active {
background-color: white;
border-bottom: 2px solid #3498db;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
"""
with open(os.path.join(CSS_DIR, 'corrections.css'), 'w', encoding='utf-8') as f:
f.write(css_content)
def create_js(errors_by_chapter):
"""Crée le fichier JavaScript pour la page de corrections."""
# Convertir les données en JSON pour les utiliser dans le JavaScript
errors_json = json.dumps(errors_by_chapter, ensure_ascii=False)
js_content = f"""
// Données des erreurs
const errorsByChapter = {errors_json};
// Fonction pour appliquer une correction
function applyCorrection(errorType, chapterTitle, errorIndex, correction) {{
// Préparer les données à envoyer
const data = {{
action: 'apply_correction',
error_type: errorType,
chapter: chapterTitle,
error_index: errorIndex,
correction: correction
}};
// Envoyer la requête au serveur
fetch('/api/corrections', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify(data)
}})
.then(response => response.json())
.then(data => {{
if (data.success) {{
// Afficher un message de succès
const card = document.querySelector(`#${{errorType}}-${{chapterTitle.replace(/\\s+/g, '-')}}-${{errorIndex}}`);
const successMsg = card.querySelector('.success-message');
successMsg.textContent = 'Correction appliquée avec succès!';
successMsg.style.display = 'block';
// Masquer le message après 3 secondes
setTimeout(() => {{
successMsg.style.display = 'none';
}}, 3000);
// Désactiver les boutons de suggestion
const suggestionBtns = card.querySelectorAll('.suggestion-btn');
suggestionBtns.forEach(btn => {{
btn.disabled = true;
btn.style.opacity = '0.5';
}});
}} else {{
// Afficher un message d'erreur
const card = document.querySelector(`#${{errorType}}-${{chapterTitle.replace(/\\s+/g, '-')}}-${{errorIndex}}`);
const errorMsg = card.querySelector('.error-message');
errorMsg.textContent = data.message || 'Erreur lors de l\\'application de la correction.';
errorMsg.style.display = 'block';
// Masquer le message après 3 secondes
setTimeout(() => {{
errorMsg.style.display = 'none';
}}, 3000);
}}
}})
.catch(error => {{
console.error('Erreur:', error);
alert('Une erreur est survenue lors de la communication avec le serveur.');
}});
}}
// Fonction pour ignorer une erreur
function ignoreError(errorType, chapterTitle, errorIndex) {{
// Préparer les données à envoyer
const data = {{
action: 'ignore_error',
error_type: errorType,
chapter: chapterTitle,
error_index: errorIndex
}};
// Envoyer la requête au serveur
fetch('/api/corrections', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify(data)
}})
.then(response => response.json())
.then(data => {{
if (data.success) {{
// Afficher un message de succès
const card = document.querySelector(`#${{errorType}}-${{chapterTitle.replace(/\\s+/g, '-')}}-${{errorIndex}}`);
const successMsg = card.querySelector('.success-message');
successMsg.textContent = 'Erreur ignorée avec succès!';
successMsg.style.display = 'block';
// Masquer le message après 3 secondes
setTimeout(() => {{
card.style.display = 'none';
}}, 1000);
}} else {{
// Afficher un message d'erreur
const card = document.querySelector(`#${{errorType}}-${{chapterTitle.replace(/\\s+/g, '-')}}-${{errorIndex}}`);
const errorMsg = card.querySelector('.error-message');
errorMsg.textContent = data.message || 'Erreur lors de l\\'ignorance de l\\'erreur.';
errorMsg.style.display = 'block';
// Masquer le message après 3 secondes
setTimeout(() => {{
errorMsg.style.display = 'none';
}}, 3000);
}}
}})
.catch(error => {{
console.error('Erreur:', error);
alert('Une erreur est survenue lors de la communication avec le serveur.');
}});
}}
// Fonction pour ajouter un mot au dictionnaire personnalisé
function addToDictionary(word) {{
// Préparer les données à envoyer
const data = {{
action: 'add_to_dictionary',
word: word
}};
// Envoyer la requête au serveur
fetch('/api/corrections', {{
method: 'POST',
headers: {{
'Content-Type': 'application/json',
}},
body: JSON.stringify(data)
}})
.then(response => response.json())
.then(data => {{
if (data.success) {{
// Masquer toutes les cartes d'erreur pour ce mot
const cards = document.querySelectorAll(`.error-card[data-word="${{word}}"]`);
cards.forEach(card => {{
const successMsg = card.querySelector('.success-message');
successMsg.textContent = 'Mot ajouté au dictionnaire personnalisé!';
successMsg.style.display = 'block';
// Masquer la carte après 1 seconde
setTimeout(() => {{
card.style.display = 'none';
}}, 1000);
}});
}} else {{
// Afficher un message d'erreur
alert(data.message || 'Erreur lors de l\\'ajout du mot au dictionnaire.');
}}
}})
.catch(error => {{
console.error('Erreur:', error);
alert('Une erreur est survenue lors de la communication avec le serveur.');
}});
}}
// Fonction pour changer d'onglet
function changeTab(tabName) {{
// Masquer tous les contenus d'onglets
const tabContents = document.querySelectorAll('.tab-content');
tabContents.forEach(content => {{
content.classList.remove('active');
}});
// Désactiver tous les onglets
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {{
tab.classList.remove('active');
}});
// Activer l'onglet sélectionné
document.getElementById(tabName).classList.add('active');
document.querySelector(`.tab[data-tab="${{tabName}}"]`).classList.add('active');
}}
// Initialiser la page quand elle est chargée
document.addEventListener('DOMContentLoaded', () => {{
// Activer le premier onglet par défaut
changeTab('spelling-tab');
// Ajouter des écouteurs d'événements pour les onglets
const tabs = document.querySelectorAll('.tab');
tabs.forEach(tab => {{
tab.addEventListener('click', () => {{
changeTab(tab.getAttribute('data-tab'));
}});
}});
}});
"""
with open(os.path.join(JS_DIR, 'corrections.js'), 'w', encoding='utf-8') as f:
f.write(js_content)
def create_html(errors_by_chapter):
"""Crée le fichier HTML pour la page de corrections."""
html_content = f"""<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Corrections Orthographiques et Grammaticales</title>
<link rel="stylesheet" href="static/css/corrections.css">
</head>
<body>
<header>
<h1>Corrections Orthographiques et Grammaticales</h1>
<p>Dernière mise à jour: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
</header>
<div class="container">
<div class="tabs">
<div class="tab active" data-tab="spelling-tab">Erreurs d'orthographe</div>
<div class="tab" data-tab="grammar-tab">Erreurs grammaticales</div>
</div>
<div id="spelling-tab" class="tab-content active">
<h2>Erreurs d'orthographe</h2>
{generate_spelling_html(errors_by_chapter)}
</div>
<div id="grammar-tab" class="tab-content">
<h2>Erreurs grammaticales</h2>
{generate_grammar_html(errors_by_chapter)}
</div>
</div>
<script src="static/js/corrections.js"></script>
</body>
</html>
"""
with open(os.path.join('build', 'corrections.html'), 'w', encoding='utf-8') as f:
f.write(html_content)
def generate_spelling_html(errors_by_chapter):
"""Génère le HTML pour les erreurs d'orthographe."""
html = ""
for chapter_title, errors in errors_by_chapter.items():
spelling_errors = errors['spelling']
if spelling_errors:
html += f"""
<div class="chapter-section">
<h3>{chapter_title}</h3>
"""
for i, error in enumerate(spelling_errors):
word = error['word']
suggestions = error['suggestions']
html += f"""
<div id="spelling-{chapter_title.replace(' ', '-')}-{i}" class="error-card spelling" data-word="{word}">
<h4>Mot mal orthographié: <span class="error-word">{word}</span></h4>
<div class="suggestion-buttons">
"""
if suggestions:
for suggestion in suggestions:
html += f"""
<button class="suggestion-btn" onclick="applyCorrection('spelling', '{chapter_title}', {i}, '{suggestion}')">{suggestion}</button>
"""
else:
html += """
<p>Aucune suggestion disponible</p>
"""
html += f"""
</div>
<div class="action-buttons">
<button class="action-btn ignore" onclick="ignoreError('spelling', '{chapter_title}', {i})">Ignorer cette erreur</button>
<button class="action-btn dictionary" onclick="addToDictionary('{word}')">Ajouter au dictionnaire</button>
</div>
<div class="success-message"></div>
<div class="error-message"></div>
</div>
"""
html += """
</div>
"""
return html
def generate_grammar_html(errors_by_chapter):
"""Génère le HTML pour les erreurs grammaticales."""
html = ""
for chapter_title, errors in errors_by_chapter.items():
grammar_errors = errors['grammar']
if grammar_errors:
html += f"""
<div class="chapter-section">
<h3>{chapter_title}</h3>
"""
for i, error in enumerate(grammar_errors):
message = error['message']
context = error['context']
error_text = error['error_text']
suggestions = error['suggestions']
# Mettre en évidence l'erreur dans le contexte
highlighted_context = context.replace(error_text, f'<span class="error-word">{error_text}</span>')
html += f"""
<div id="grammar-{chapter_title.replace(' ', '-')}-{i}" class="error-card grammar">
<h4>{message}</h4>
<div class="error-context">
{highlighted_context}
</div>
<div class="suggestion-buttons">
"""
if suggestions:
for suggestion in suggestions:
html += f"""
<button class="suggestion-btn" onclick="applyCorrection('grammar', '{chapter_title}', {i}, '{suggestion}')">{suggestion}</button>
"""
else:
html += """
<p>Aucune suggestion disponible</p>
"""
html += f"""
</div>
<div class="action-buttons">
<button class="action-btn ignore" onclick="ignoreError('grammar', '{chapter_title}', {i})">Ignorer cette erreur</button>
</div>
<div class="success-message"></div>
<div class="error-message"></div>
</div>
"""
html += """
</div>
"""
return html
def main():
print("Génération de la page de corrections...")
# Définir les chemins des fichiers
report_path = 'rapport_orthographe_grammaire.md'
# Vérifier si le rapport existe
if not os.path.exists(report_path):
print(f"Erreur: Le fichier {report_path} n'existe pas.")
return
# Parser le rapport d'erreurs
errors_by_chapter = parse_error_report(report_path)
# Créer les fichiers CSS et JS
create_css()
create_js(errors_by_chapter)
# Créer le fichier HTML
create_html(errors_by_chapter)
print("Page de corrections générée avec succès dans le dossier 'build'.")
print("Fichier généré: build/corrections.html")
if __name__ == "__main__":
main()