book-generator-orgmode/generate_corrections_page.py
2025-08-31 22:37:24 +02:00

861 lines
No EOL
30 KiB
Python
Executable file

#!/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)
# Nouveau motif pour extraire les erreurs d'orthographe avec contexte étendu
word_pattern = r'- \*\*(.*?)\*\*: (.*?)(?=\n- \*\*|\Z)'
for word_match in re.finditer(word_pattern, spelling_text, re.DOTALL):
word = word_match.group(1)
suggestions_text = word_match.group(2).strip()
suggestions = []
if suggestions_text != "Aucune suggestion":
suggestions = [s.strip() for s in suggestions_text.split(',')]
# Extraire les occurrences pour ce mot
occurrence_text = word_match.group(0)
occurrence_pattern = r' - \*\*Occurrence (\d+)\*\*:\n - \*\*Contexte\*\*: (.*?)\n - \*\*Contexte étendu\*\*: ```\n(.*?)\n```'
occurrences = re.finditer(occurrence_pattern, occurrence_text, re.DOTALL)
# S'il y a des occurrences, les traiter
occurrences_found = False
for occ_match in occurrences:
occurrences_found = True
occ_num = occ_match.group(1)
context = occ_match.group(2).strip()
extended_context = occ_match.group(3).strip()
# Extraire le texte en gras du contexte (le mot mal orthographié)
error_text_match = re.search(r'\*\*(.*?)\*\*', context)
error_text = error_text_match.group(1) if error_text_match else word
# Nettoyer le contexte pour l'affichage
clean_context = re.sub(r'\*\*(.*?)\*\*', r'\1', context)
spelling_errors.append({
'word': word,
'context': clean_context,
'extended_context': extended_context,
'error_text': error_text,
'suggestions': suggestions,
'type': 'spelling'
})
# Si aucune occurrence n'a été trouvée, ajouter une entrée simple
if not occurrences_found:
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)
# Nouveau motif pour extraire les erreurs grammaticales avec contexte étendu
error_pattern = r'- \*\*Erreur (\d+)\*\*: (.*?)\n - \*\*Contexte\*\*: (.*?)(?:\n - \*\*Contexte étendu\*\*: ```\n(.*?)\n```)?(?:\n - \*\*Suggestions\*\*: (.*?))?(?=\n\n- \*\*Erreur|\Z)'
for error_match in re.finditer(error_pattern, grammar_text, re.DOTALL):
error_num = error_match.group(1)
message = error_match.group(2).strip()
context = error_match.group(3).strip()
extended_context = error_match.group(4).strip() if error_match.group(4) else None
suggestions_text = error_match.group(5).strip() if error_match.group(5) else "Aucune suggestion"
# 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_error = {
'message': message,
'context': clean_context,
'error_text': error_text,
'suggestions': suggestions,
'type': 'grammar'
}
# Ajouter le contexte étendu s'il existe
if extended_context:
grammar_error['extended_context'] = extended_context
grammar_errors.append(grammar_error)
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, h4, h5 {
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;
}
.extended-context {
background-color: #f0f0f0;
padding: 10px;
border-radius: 5px;
margin: 10px 0;
border-left: 3px solid #7f8c8d;
}
.extended-context h5 {
margin-top: 0;
color: #7f8c8d;
font-size: 14px;
}
.extended-context pre {
margin: 0;
font-family: monospace;
white-space: pre-wrap;
line-height: 1.5;
font-size: 13px;
color: #555;
}
.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;
}
.custom-correction {
margin-top: 15px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 5px;
}
.custom-correction h5 {
margin-top: 0;
margin-bottom: 10px;
color: #555;
font-size: 14px;
}
.input-group {
display: flex;
gap: 10px;
}
.custom-input {
flex-grow: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.custom-btn {
background-color: #9b59b6;
color: white;
border: none;
border-radius: 4px;
padding: 8px 15px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
}
.custom-btn:hover {
background-color: #8e44ad;
}
.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';
}});
// Désactiver le champ de correction personnalisée
const customInput = card.querySelector('.custom-input');
const customBtn = card.querySelector('.custom-btn');
if (customInput) customInput.disabled = true;
if (customBtn) customBtn.disabled = true;
}} 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 appliquer une correction personnalisée
function applyCustomCorrection(errorType, chapterTitle, errorIndex) {{
// Récupérer la valeur de la correction personnalisée
const inputId = `custom-${{errorType}}-${{chapterTitle.replace(/\\s+/g, '-')}}-${{errorIndex}}`;
const customInput = document.getElementById(inputId);
if (!customInput || !customInput.value.trim()) {{
alert('Veuillez entrer une correction.');
return;
}}
// Appliquer la correction
applyCorrection(errorType, chapterTitle, errorIndex, customInput.value.trim());
}}
// Ajouter des écouteurs d'événements pour les champs de saisie personnalisée
function setupCustomInputs() {{
const customInputs = document.querySelectorAll('.custom-input');
customInputs.forEach(input => {{
input.addEventListener('keypress', (e) => {{
if (e.key === 'Enter') {{
// Extraire les informations de l'ID
const inputId = input.id;
const parts = inputId.replace('custom-', '').split('-');
const errorType = parts[0];
const errorIndex = parts[parts.length - 1];
const chapterTitle = parts.slice(1, parts.length - 1).join(' ');
// Appliquer la correction
applyCustomCorrection(errorType, chapterTitle, errorIndex);
}}
}});
}});
}}
// 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'));
}});
}});
// Configurer les champs de saisie personnalisée
setupCustomInputs();
}});
"""
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>
"""
# Ajouter le contexte s'il existe
if 'context' in error:
html += f"""
<div class="error-context">
{error['context'].replace(word, f'<span class="error-word">{word}</span>')}
</div>
"""
# Ajouter le contexte étendu s'il existe
if 'extended_context' in error:
html += f"""
<div class="extended-context">
<h5>Contexte étendu:</h5>
<pre>{error['extended_context']}</pre>
</div>
"""
html += f"""
<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="custom-correction">
<h5>Correction personnalisée:</h5>
<div class="input-group">
<input type="text" id="custom-spelling-{chapter_title.replace(' ', '-')}-{i}" class="custom-input" placeholder="Entrez votre correction">
<button class="custom-btn" onclick="applyCustomCorrection('spelling', '{chapter_title}', {i})">Appliquer</button>
</div>
</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>
"""
# Ajouter le contexte étendu s'il existe
if 'extended_context' in error:
html += f"""
<div class="extended-context">
<h5>Contexte étendu:</h5>
<pre>{error['extended_context']}</pre>
</div>
"""
html += f"""
<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="custom-correction">
<h5>Correction personnalisée:</h5>
<div class="input-group">
<input type="text" id="custom-grammar-{chapter_title.replace(' ', '-')}-{i}" class="custom-input" placeholder="Entrez votre correction">
<button class="custom-btn" onclick="applyCustomCorrection('grammar', '{chapter_title}', {i})">Appliquer</button>
</div>
</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()