visualisation des corrections et api flask pour modifier le livre

This commit is contained in:
Tykayn 2025-08-30 22:41:51 +02:00 committed by tykayn
parent 0680c7594e
commit 7095f9633b
14 changed files with 1593 additions and 276 deletions

View file

@ -77,8 +77,34 @@ Analyse les fautes d'orthographe et de grammaire dans votre livre et génère:
- Un rapport détaillé au format Markdown des erreurs trouvées dans chaque chapitre
- Un fichier CSV résumant le nombre d'erreurs par chapitre
- Des suggestions de correction pour chaque erreur identifiée
- Utilise un dictionnaire personnalisé pour exclure certains mots de la vérification
Cet outil vous aide à améliorer la qualité linguistique de votre texte en identifiant les problèmes potentiels chapitre par chapitre.
#### Interface de correction interactive
`python generate_corrections_page.py`
Génère une page web interactive dans le dossier build qui permet de:
- Visualiser les erreurs orthographiques et grammaticales dans leur contexte
- Appliquer directement les corrections suggérées au fichier livre.org
- Ignorer certaines erreurs en les marquant comme "à ne pas traiter"
- Ajouter des mots au dictionnaire personnalisé
Pour utiliser l'interface de correction:
1. Exécutez `python generate_corrections_page.py` pour générer la page
2. Lancez le serveur Flask avec `python app.py`
3. Accédez à http://localhost:5000/build/corrections.html dans votre navigateur
Cet outil vous aide à améliorer la qualité linguistique de votre texte en identifiant les problèmes potentiels chapitre par chapitre et en facilitant leur correction.
### Analyse des enchaînements d'intrigue
`python analyse_intrigues.py`
Analyse les enchaînements d'intrigue dans votre livre et génère:
- Un graphique avec une ligne unique composée de segments colorés représentant chaque intrigue
- Un diagramme de Gantt coloré des intrigues
- Une page web interactive dans le dossier build qui présente:
- Un graphique interactif des intrigues avec informations au survol
- Une liste hiérarchique des intrigues et sous-intrigues
- Les visualisations PNG et SVG générées
Cet outil vous aide à visualiser comment vos intrigues s'enchaînent et s'imbriquent tout au long de votre livre. Les visualisations sont également intégrées au tableau de bord web existant.
### Statistiques
`bash up_infos.sh`
@ -134,7 +160,11 @@ Un tableau de bord web interactif est disponible pour visualiser les données de
- Graphique de progression de l'écriture
- Graphique de réseau des personnages (déplaçable à la souris)
- Graphique de réseau reliant les personnages aux intrigues (déplaçable à la souris, les labels des personnages sont toujours visibles, les labels des intrigues ne sont visibles que lors du survol)
- Chronologie des intrigues
- Chronologie des intrigues avec:
- Visualisation interactive des intrigues
- Enchaînement des intrigues avec segments colorés
- Diagramme de Gantt coloré des intrigues
- Lien vers une page d'analyse détaillée des intrigues
Pour générer le tableau de bord, exécutez:
```bash

470
analyse_intrigues.py Normal file
View file

@ -0,0 +1,470 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Script d'analyse des enchaînements d'intrigue dans le livre.
Ce script:
1. Analyse le CSV des intrigues
2. Attribue une couleur à chaque ligne d'intrigue selon un gradient rouge-vert (rouge pour le début, vert pour la fin)
3. Réalise un graphique avec une seule ligne faite de segments colorés
4. Sauvegarde le graphique en PNG et SVG
5. Crée une page web interactive dans le dossier build
"""
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
import numpy as np
import os
import json
from matplotlib.colors import to_hex
import shutil
# Créer le dossier build s'il n'existe pas
if not os.path.exists('build'):
os.makedirs('build')
# Fonction pour générer un gradient de couleurs du rouge au vert
def generate_red_to_green_gradient(n):
"""Génère n couleurs dans un gradient allant du rouge au vert"""
# Utiliser un gradient de rouge (début) à vert (fin)
colors = []
for i in range(n):
# Calculer la position dans le gradient (0 = rouge, 1 = vert)
t = i / max(1, n - 1)
# Rouge: diminue de 1 à 0
r = 1.0 - t
# Vert: augmente de 0 à 1
g = t
# Bleu: toujours à 0
b = 0.0
colors.append(np.array([r, g, b, 1.0]))
return colors
# Lire le fichier CSV des intrigues
df = pd.read_csv('intrigues.csv')
# Trier les intrigues par début
df = df.sort_values(by=['Début', 'Fin'])
# Identifier les intrigues principales et les sous-intrigues
# Une sous-intrigue est contenue dans une autre intrigue si son début et sa fin
# sont compris dans l'intervalle de l'intrigue parente
def identify_subplots(df):
"""Identifie les relations parent-enfant entre les intrigues"""
# Initialiser une colonne pour le parent
df['Parent'] = None
# Pour chaque intrigue, trouver son parent (s'il existe)
for i, row in df.iterrows():
# Chercher parmi toutes les autres intrigues
for j, potential_parent in df.iterrows():
if i != j: # Ne pas comparer avec soi-même
# Si l'intrigue est contenue dans une autre
if (row['Début'] >= potential_parent['Début'] and
row['Fin'] <= potential_parent['Fin'] and
# S'assurer que ce n'est pas exactement le même intervalle
not (row['Début'] == potential_parent['Début'] and
row['Fin'] == potential_parent['Fin'])):
# Si on a déjà trouvé un parent, prendre le plus spécifique
if df.at[i, 'Parent'] is not None:
current_parent_idx = df.index[df['Intrigue'] == df.at[i, 'Parent']].tolist()[0]
current_parent = df.iloc[current_parent_idx]
# Le parent le plus spécifique est celui avec l'intervalle le plus court
if ((potential_parent['Fin'] - potential_parent['Début']) <
(current_parent['Fin'] - current_parent['Début'])):
df.at[i, 'Parent'] = potential_parent['Intrigue']
else:
df.at[i, 'Parent'] = potential_parent['Intrigue']
return df
# Appliquer la fonction pour identifier les sous-intrigues
df = identify_subplots(df)
# Organiser les intrigues en groupes basés sur les relations parent-enfant
def organize_plots_by_hierarchy(df):
# Créer un dictionnaire pour stocker les groupes d'intrigues
plot_groups = {}
group_id = 0
# D'abord, identifier toutes les intrigues principales (sans parent)
main_plots = df[df['Parent'].isna()]['Intrigue'].tolist()
# Pour chaque intrigue principale, créer un groupe avec ses sous-intrigues
for main_plot in main_plots:
group = [main_plot]
# Trouver toutes les sous-intrigues directes
subplots = df[df['Parent'] == main_plot]['Intrigue'].tolist()
group.extend(subplots)
# Trouver les sous-intrigues de niveau 2 (sous-intrigues des sous-intrigues)
for subplot in subplots:
sub_subplots = df[df['Parent'] == subplot]['Intrigue'].tolist()
group.extend(sub_subplots)
plot_groups[group_id] = group
group_id += 1
# Créer un dictionnaire qui associe chaque intrigue à son groupe
plot_to_group = {}
for group_id, plots in plot_groups.items():
for plot in plots:
plot_to_group[plot] = group_id
return plot_groups, plot_to_group
# Déterminer les valeurs min et max pour les chapitres
min_chapter = df['Début'].min()
max_chapter = df['Fin'].max()
chapter_range = max_chapter - min_chapter
# Créer un dictionnaire de couleurs pour chaque intrigue
color_dict = {}
# Trier les intrigues par position dans l'histoire (début du chapitre)
sorted_intrigues = df.sort_values(by='Début')
# Générer un gradient de couleurs du rouge au vert
gradient_colors = generate_red_to_green_gradient(len(df))
# Assigner les couleurs aux intrigues en fonction de leur position dans l'histoire
for i, (_, row) in enumerate(sorted_intrigues.iterrows()):
intrigue = row['Intrigue']
# Utiliser la position normalisée dans l'histoire pour déterminer la couleur
position = (row['Début'] - min_chapter) / chapter_range if chapter_range > 0 else 0
# Assigner la couleur du gradient correspondant à la position
color_index = min(int(position * (len(gradient_colors) - 1)), len(gradient_colors) - 1)
color_dict[intrigue] = to_hex(gradient_colors[i])
# Créer un graphique avec une ligne unique composée de segments colorés
plt.figure(figsize=(12, 4))
# Tracer chaque segment d'intrigue
for i, row in df.iterrows():
plt.plot([row['Début'], row['Fin']], [1, 1], linewidth=10,
color=color_dict[row['Intrigue']],
solid_capstyle='butt',
label=row['Intrigue'])
# Ajouter des marqueurs pour les points de début et fin
for i, row in df.iterrows():
plt.plot(row['Début'], 1, 'o', color='black', markersize=5)
plt.plot(row['Fin'], 1, 'o', color='black', markersize=5)
# Configurer les axes
plt.yticks([]) # Cacher l'axe y
plt.xlabel('Chapitres')
plt.title('Enchaînement des intrigues')
# Ajouter une légende unique pour chaque intrigue
handles, labels = plt.gca().get_legend_handles_labels()
by_label = dict(zip(labels, handles))
plt.legend(by_label.values(), by_label.keys(), loc='upper center',
bbox_to_anchor=(0.5, -0.15), ncol=3)
# Ajuster les marges
plt.tight_layout()
# Sauvegarder en PNG et SVG
plt.savefig('build/enchaînement_intrigues.png', dpi=300, bbox_inches='tight')
plt.savefig('build/enchaînement_intrigues.svg', bbox_inches='tight')
# Créer un diagramme de Gantt (comme dans gantt_parser.py mais avec des couleurs)
fig, ax = plt.subplots(figsize=(12, 8))
# Tracer chaque intrigue avec sa couleur
for i, row in df.iterrows():
ax.plot([row['Début'], row['Fin']], [i, i], '-',
color=color_dict[row['Intrigue']],
linewidth=10)
ax.plot([row['Début'], row['Début']], [i-0.1, i+0.1], 'ko')
ax.plot([row['Fin'], row['Fin']], [i-0.1, i+0.1], 'ko')
# Configurer les axes
ax.set_yticks(range(len(df)))
ax.set_yticklabels(df['Intrigue'])
ax.set_xticks(range(int(df['Début'].min()), int(df['Fin'].max())+1))
ax.set_xticklabels(ax.get_xticks())
ax.set_xlabel('Chapitres')
ax.set_title('Diagramme de Gantt des intrigues')
# Ajuster les marges
plt.tight_layout()
# Sauvegarder en PNG et SVG
plt.savefig('build/gantt_intrigues.png', dpi=300, bbox_inches='tight')
plt.savefig('build/gantt_intrigues.svg', bbox_inches='tight')
# Préparer les données pour la page web interactive
web_data = []
for i, row in df.iterrows():
web_data.append({
'intrigue': row['Intrigue'],
'debut': int(row['Début']),
'fin': int(row['Fin']),
'color': color_dict[row['Intrigue']],
'parent': row['Parent']
})
# Sauvegarder les données en JSON
with open('build/intrigues_data.json', 'w', encoding='utf-8') as f:
json.dump(web_data, f, ensure_ascii=False, indent=2)
# Créer la page HTML interactive pour les intrigues
html_content = """<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Analyse des Intrigues</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background-color: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
h1, h2 {
color: #333;
}
.plot-timeline {
height: 100px;
position: relative;
margin: 30px 0;
background-color: #eee;
border-radius: 5px;
}
.plot-segment {
position: absolute;
height: 30px;
top: 35px;
border-radius: 5px;
cursor: pointer;
transition: all 0.3s;
}
.plot-segment:hover {
transform: translateY(-5px);
box-shadow: 0 5px 10px rgba(0,0,0,0.2);
}
.timeline-marker {
position: absolute;
bottom: 0;
width: 1px;
height: 10px;
background-color: #333;
}
.timeline-label {
position: absolute;
bottom: -20px;
transform: translateX(-50%);
font-size: 12px;
}
.plot-list {
margin-top: 50px;
}
.plot-item {
margin-bottom: 10px;
padding: 10px;
border-radius: 5px;
}
.subplots {
margin-left: 30px;
}
.plot-info {
display: none;
margin-top: 20px;
padding: 10px;
background-color: #f9f9f9;
border-radius: 5px;
border-left: 5px solid #333;
}
.images {
display: flex;
flex-direction: column;
gap: 20px;
margin-top: 30px;
}
img {
max-width: 100%;
height: auto;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div class="container">
<h1>Analyse des Intrigues</h1>
<h2>Enchaînement des intrigues</h2>
<div id="plot-timeline" class="plot-timeline"></div>
<div id="plot-info" class="plot-info"></div>
<h2>Liste des intrigues</h2>
<div id="plot-list" class="plot-list"></div>
<div class="images">
<h2>Visualisations</h2>
<div>
<h3>Enchaînement des intrigues</h3>
<img src="enchaînement_intrigues.png" alt="Enchaînement des intrigues">
</div>
<div>
<h3>Diagramme de Gantt des intrigues</h3>
<img src="gantt_intrigues.png" alt="Diagramme de Gantt des intrigues">
</div>
</div>
</div>
<script>
// Charger les données
fetch('intrigues_data.json')
.then(response => response.json())
.then(data => {
// Trouver les valeurs min et max pour l'échelle
const minChapter = Math.min(...data.map(item => item.debut));
const maxChapter = Math.max(...data.map(item => item.fin));
const timelineWidth = document.getElementById('plot-timeline').offsetWidth;
// Fonction pour convertir un numéro de chapitre en position X
function chapterToX(chapter) {
return ((chapter - minChapter) / (maxChapter - minChapter)) * timelineWidth;
}
// Ajouter les marqueurs de chapitre
const timeline = document.getElementById('plot-timeline');
for (let i = minChapter; i <= maxChapter; i++) {
const marker = document.createElement('div');
marker.className = 'timeline-marker';
marker.style.left = chapterToX(i) + 'px';
const label = document.createElement('div');
label.className = 'timeline-label';
label.textContent = i;
label.style.left = chapterToX(i) + 'px';
timeline.appendChild(marker);
timeline.appendChild(label);
}
// Ajouter les segments d'intrigue
data.forEach(plot => {
const segment = document.createElement('div');
segment.className = 'plot-segment';
segment.style.left = chapterToX(plot.debut) + 'px';
segment.style.width = (chapterToX(plot.fin) - chapterToX(plot.debut)) + 'px';
segment.style.backgroundColor = plot.color;
segment.setAttribute('data-intrigue', plot.intrigue);
// Ajouter un événement au survol
segment.addEventListener('mouseover', () => {
const plotInfo = document.getElementById('plot-info');
plotInfo.style.display = 'block';
plotInfo.style.borderLeftColor = plot.color;
plotInfo.innerHTML = `
<h3>${plot.intrigue}</h3>
<p>Chapitres ${plot.debut} à ${plot.fin}</p>
${plot.parent ? `<p>Fait partie de: ${plot.parent}</p>` : ''}
`;
});
segment.addEventListener('mouseout', () => {
document.getElementById('plot-info').style.display = 'none';
});
timeline.appendChild(segment);
});
// Organiser les intrigues en hiérarchie
const plotsWithoutParent = data.filter(plot => !plot.parent);
const plotList = document.getElementById('plot-list');
// Fonction récursive pour afficher les intrigues et leurs sous-intrigues
function displayPlots(plots, container, level = 0) {
plots.forEach(plot => {
const plotItem = document.createElement('div');
plotItem.className = 'plot-item';
plotItem.style.backgroundColor = plot.color + '33'; // Ajouter transparence
plotItem.style.borderLeft = `5px solid ${plot.color}`;
plotItem.innerHTML = `
<h3>${plot.intrigue}</h3>
<p>Chapitres ${plot.debut} à ${plot.fin}</p>
`;
container.appendChild(plotItem);
// Trouver les sous-intrigues
const subplots = data.filter(subPlot => subPlot.parent === plot.intrigue);
if (subplots.length > 0) {
const subplotsContainer = document.createElement('div');
subplotsContainer.className = 'subplots';
plotItem.appendChild(subplotsContainer);
displayPlots(subplots, subplotsContainer, level + 1);
}
});
}
// Afficher les intrigues principales et leurs sous-intrigues
displayPlots(plotsWithoutParent, plotList);
})
.catch(error => console.error('Erreur lors du chargement des données:', error));
</script>
</body>
</html>
"""
# Écrire le fichier HTML pour les intrigues
with open('build/intrigues.html', 'w', encoding='utf-8') as f:
f.write(html_content)
# Supprimer l'ancien index.html s'il existe
if os.path.exists('build/index.html'):
os.remove('build/index.html')
# Mettre à jour le dashboard.html pour inclure un lien vers la page des intrigues
# et ajouter les visualisations d'intrigues
if os.path.exists('build/dashboard.html'):
with open('build/dashboard.html', 'r', encoding='utf-8') as f:
dashboard_content = f.read()
# Ajouter un lien vers la page des intrigues dans la section des intrigues
if '<div class="dashboard-section">\n <h2>Chronologie des Intrigues</h2>' in dashboard_content:
dashboard_content = dashboard_content.replace(
'<div class="dashboard-section">\n <h2>Chronologie des Intrigues</h2>',
'<div class="dashboard-section">\n <h2>Chronologie des Intrigues</h2>\n <p><a href="intrigues.html">Voir l\'analyse détaillée des intrigues</a></p>'
)
# Ajouter les nouvelles visualisations à la section des intrigues
if '<div class="chart-container" id="plot-timeline"></div>' in dashboard_content:
dashboard_content = dashboard_content.replace(
'<div class="chart-container" id="plot-timeline"></div>',
'<div class="chart-container" id="plot-timeline"></div>\n <h3>Enchaînement des intrigues</h3>\n <div class="chart-container">\n <img src="enchaînement_intrigues.png" alt="Enchaînement des intrigues" style="max-width:100%;">\n </div>\n <h3>Diagramme de Gantt des intrigues</h3>\n <div class="chart-container">\n <img src="gantt_intrigues.png" alt="Diagramme de Gantt des intrigues" style="max-width:100%;">\n </div>'
)
# Écrire le fichier dashboard mis à jour
with open('build/dashboard.html', 'w', encoding='utf-8') as f:
f.write(dashboard_content)
print("Analyse des intrigues terminée.")
print("Fichiers générés dans le dossier 'build':")
print("- enchaînement_intrigues.png")
print("- enchaînement_intrigues.svg")
print("- gantt_intrigues.png")
print("- gantt_intrigues.svg")
print("- intrigues_data.json")
print("- intrigues.html")

View file

@ -15,7 +15,7 @@ import re
import os
import csv
import argparse
from pyspellchecker import SpellChecker
from spellchecker import SpellChecker
import language_tool_python
# Définir les arguments en ligne de commande
@ -71,12 +71,38 @@ def clean_chapter_content(content):
return content.strip()
def check_spelling(text, lang='fr'):
def load_custom_dictionary(file_path):
"""
Charge le dictionnaire personnalisé à partir d'un fichier texte.
Retourne un ensemble de mots à considérer comme corrects.
"""
custom_words = set()
# Vérifier si le fichier existe
if os.path.exists(file_path):
with open(file_path, 'r', encoding='utf-8') as file:
for line in file:
# Ignorer les lignes vides et les commentaires
line = line.strip()
if line and not line.startswith('#'):
custom_words.add(line.lower())
return custom_words
def check_spelling(text, lang='fr', custom_dict_path='dictionnaire_personnalise.txt'):
"""
Vérifie l'orthographe d'un texte et retourne les mots mal orthographiés.
Utilise un dictionnaire personnalisé pour exclure certains mots de la vérification.
"""
spell = SpellChecker(language=lang)
# Charger le dictionnaire personnalisé
custom_words = load_custom_dictionary(custom_dict_path)
# Ajouter les mots du dictionnaire personnalisé au dictionnaire du vérificateur
if custom_words:
spell.word_frequency.load_words(custom_words)
# Diviser le texte en mots
words = re.findall(r'\b\w+\b', text.lower())
@ -86,10 +112,15 @@ def check_spelling(text, lang='fr'):
# Créer un dictionnaire avec les mots mal orthographiés et leurs suggestions
spelling_errors = {}
for word in misspelled:
# Vérifier si le mot est dans le dictionnaire personnalisé
if word in custom_words:
continue
# Obtenir les suggestions de correction
suggestions = spell.candidates(word)
# Limiter à 5 suggestions maximum
suggestions_list = list(suggestions)[:5]
suggestions_list = list(suggestions) if suggestions is not None else []
suggestions_list = suggestions_list[:5]
spelling_errors[word] = suggestions_list
return spelling_errors

242
api_corrections.py Normal file
View file

@ -0,0 +1,242 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
API pour traiter les requêtes de correction depuis la page web.
Ce script:
1. Fournit des endpoints pour appliquer des corrections au fichier livre.org
2. Permet d'ajouter des mots au dictionnaire personnalisé
3. Permet de marquer des erreurs comme "à ne pas traiter" dans le CSV
"""
import os
import re
import json
import csv
from flask import Flask, request, jsonify, Blueprint
# Créer un Blueprint pour l'API
corrections_api = Blueprint('corrections_api', __name__)
# Chemin vers les fichiers
LIVRE_PATH = 'livre.org'
DICT_PATH = 'dictionnaire_personnalise.txt'
CSV_PATH = 'resume_erreurs.csv'
@corrections_api.route('/api/corrections', methods=['POST'])
def handle_corrections():
"""Endpoint pour traiter les requêtes de correction."""
data = request.json
if not data or 'action' not in data:
return jsonify({'success': False, 'message': 'Action non spécifiée'}), 400
action = data['action']
if action == 'apply_correction':
return apply_correction(data)
elif action == 'ignore_error':
return ignore_error(data)
elif action == 'add_to_dictionary':
return add_to_dictionary(data)
else:
return jsonify({'success': False, 'message': 'Action non reconnue'}), 400
def apply_correction(data):
"""Applique une correction au fichier livre.org."""
# Vérifier les paramètres requis
required_params = ['error_type', 'chapter', 'error_index', 'correction']
if not all(param in data for param in required_params):
return jsonify({'success': False, 'message': 'Paramètres manquants'}), 400
error_type = data['error_type']
chapter = data['chapter']
error_index = data['error_index']
correction = data['correction']
try:
# Lire le contenu du fichier
with open(LIVRE_PATH, 'r', encoding='utf-8') as file:
content = file.read()
# Trouver la section du chapitre
chapter_pattern = r'^\*\* ' + re.escape(chapter) + r'$(.*?)(?=^\*\* |\Z)'
chapter_match = re.search(chapter_pattern, content, re.MULTILINE | re.DOTALL)
if not chapter_match:
return jsonify({'success': False, 'message': f'Chapitre "{chapter}" non trouvé'}), 404
chapter_content = chapter_match.group(1)
chapter_start = chapter_match.start()
chapter_end = chapter_match.end()
# Appliquer la correction en fonction du type d'erreur
if error_type == 'spelling':
# Charger les erreurs d'orthographe pour ce chapitre
errors = load_spelling_errors(chapter)
if error_index >= len(errors):
return jsonify({'success': False, 'message': 'Index d\'erreur invalide'}), 400
error = errors[error_index]
word = error['word']
# Remplacer le mot dans le contenu du chapitre
modified_chapter = re.sub(r'\b' + re.escape(word) + r'\b', correction, chapter_content)
# Mettre à jour le contenu complet
new_content = content[:chapter_start] + '** ' + chapter + modified_chapter + content[chapter_end:]
elif error_type == 'grammar':
# Charger les erreurs grammaticales pour ce chapitre
errors = load_grammar_errors(chapter)
if error_index >= len(errors):
return jsonify({'success': False, 'message': 'Index d\'erreur invalide'}), 400
error = errors[error_index]
error_text = error['error_text']
context = error['context']
# Trouver le contexte dans le chapitre
context_pattern = re.escape(context.replace(error_text, error_text))
context_match = re.search(context_pattern, chapter_content)
if not context_match:
return jsonify({'success': False, 'message': 'Contexte non trouvé dans le chapitre'}), 404
# Remplacer l'erreur dans le contexte
corrected_context = context.replace(error_text, correction)
# Remplacer le contexte dans le chapitre
modified_chapter = chapter_content.replace(context, corrected_context)
# Mettre à jour le contenu complet
new_content = content[:chapter_start] + '** ' + chapter + modified_chapter + content[chapter_end:]
else:
return jsonify({'success': False, 'message': 'Type d\'erreur non reconnu'}), 400
# Écrire le contenu modifié dans le fichier
with open(LIVRE_PATH, 'w', encoding='utf-8') as file:
file.write(new_content)
return jsonify({'success': True, 'message': 'Correction appliquée avec succès'})
except Exception as e:
return jsonify({'success': False, 'message': f'Erreur lors de l\'application de la correction: {str(e)}'}), 500
def ignore_error(data):
"""Marque une erreur comme 'à ne pas traiter' dans le CSV."""
# Vérifier les paramètres requis
required_params = ['error_type', 'chapter', 'error_index']
if not all(param in data for param in required_params):
return jsonify({'success': False, 'message': 'Paramètres manquants'}), 400
error_type = data['error_type']
chapter = data['chapter']
error_index = data['error_index']
try:
# Lire le CSV des erreurs
rows = []
with open(CSV_PATH, 'r', newline='', encoding='utf-8') as csvfile:
reader = csv.reader(csvfile)
headers = next(reader) # Lire les en-têtes
# Ajouter les en-têtes à la liste des lignes
rows.append(headers)
# Parcourir les lignes et mettre à jour celle du chapitre concerné
for row in reader:
if len(row) > 0 and row[0] == chapter:
# Mettre à jour la ligne en fonction du type d'erreur
if error_type == 'spelling' and len(row) > 1:
# Décrémenter le nombre d'erreurs d'orthographe
spelling_errors = int(row[1]) - 1
row[1] = str(max(0, spelling_errors))
# Mettre à jour le total
if len(row) > 3:
row[3] = str(int(row[3]) - 1)
elif error_type == 'grammar' and len(row) > 2:
# Décrémenter le nombre d'erreurs grammaticales
grammar_errors = int(row[2]) - 1
row[2] = str(max(0, grammar_errors))
# Mettre à jour le total
if len(row) > 3:
row[3] = str(int(row[3]) - 1)
rows.append(row)
# Écrire les modifications dans le CSV
with open(CSV_PATH, 'w', newline='', encoding='utf-8') as csvfile:
writer = csv.writer(csvfile)
writer.writerows(rows)
return jsonify({'success': True, 'message': 'Erreur ignorée avec succès'})
except Exception as e:
return jsonify({'success': False, 'message': f'Erreur lors de l\'ignorance de l\'erreur: {str(e)}'}), 500
def add_to_dictionary(data):
"""Ajoute un mot au dictionnaire personnalisé."""
# Vérifier les paramètres requis
if 'word' not in data:
return jsonify({'success': False, 'message': 'Mot non spécifié'}), 400
word = data['word']
try:
# Vérifier si le mot est déjà dans le dictionnaire
existing_words = set()
if os.path.exists(DICT_PATH):
with open(DICT_PATH, 'r', encoding='utf-8') as file:
for line in file:
line = line.strip()
if line and not line.startswith('#'):
existing_words.add(line.lower())
# Si le mot n'est pas déjà dans le dictionnaire, l'ajouter
if word.lower() not in existing_words:
with open(DICT_PATH, 'a', encoding='utf-8') as file:
file.write(f"\n{word}")
return jsonify({'success': True, 'message': 'Mot ajouté au dictionnaire avec succès'})
except Exception as e:
return jsonify({'success': False, 'message': f'Erreur lors de l\'ajout du mot au dictionnaire: {str(e)}'}), 500
def load_spelling_errors(chapter):
"""Charge les erreurs d'orthographe pour un chapitre donné depuis le rapport."""
# Cette fonction devrait charger les erreurs d'orthographe depuis le rapport
# Pour simplifier, nous utilisons une implémentation fictive
# Dans une implémentation réelle, il faudrait parser le rapport d'erreurs
# Exemple d'implémentation fictive
from generate_corrections_page import parse_error_report
errors_by_chapter = parse_error_report('rapport_orthographe_grammaire.md')
if chapter in errors_by_chapter:
return errors_by_chapter[chapter]['spelling']
return []
def load_grammar_errors(chapter):
"""Charge les erreurs grammaticales pour un chapitre donné depuis le rapport."""
# Cette fonction devrait charger les erreurs grammaticales depuis le rapport
# Pour simplifier, nous utilisons une implémentation fictive
# Dans une implémentation réelle, il faudrait parser le rapport d'erreurs
# Exemple d'implémentation fictive
from generate_corrections_page import parse_error_report
errors_by_chapter = parse_error_report('rapport_orthographe_grammaire.md')
if chapter in errors_by_chapter:
return errors_by_chapter[chapter]['grammar']
return []
# Pour tester l'API indépendamment
if __name__ == '__main__':
app = Flask(__name__)
app.register_blueprint(corrections_api)
app.run(debug=True, port=5001)

19
app.py
View file

@ -1,11 +1,21 @@
from flask import Flask, render_template, request, jsonify
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Application Flask pour le générateur de livre.
"""
from flask import Flask, render_template, request, jsonify, send_from_directory
import os
import json
from datetime import datetime, timedelta
import re
from api_corrections import corrections_api
app = Flask(__name__, static_folder='static')
# Enregistrer le blueprint de l'API de corrections
app.register_blueprint(corrections_api)
def load_org_file(filename):
if os.path.exists(filename):
with open(filename, 'r', encoding='utf-8') as f:
@ -355,5 +365,10 @@ def get_characters_file():
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/build/<path:path>')
def serve_build(path):
"""Sert les fichiers statiques du dossier build."""
return send_from_directory('build', path)
if __name__ == '__main__':
app.run(debug=True)
app.run(debug=True, host='0.0.0.0', port=5000)

View file

@ -84,5 +84,9 @@
}
]
}
]
],
"2025-08-30": {
"words": 0,
"last_update": "2025-08-30 22:34:19"
}
}

View file

@ -0,0 +1,8 @@
# Dictionnaire personnalisé
# Ce fichier contient les mots à considérer comme corrects lors de l'analyse orthographique
# Un mot par ligne
# Noms propres
eryndor
# Termes spécifiques

View file

@ -1,92 +0,0 @@
#!/usr/bin/env python3
"""
Script pour réparer les fichiers CSV contenant des dates corrompues avec des caractères nuls.
Ce script lit un fichier CSV, nettoie les dates corrompues, et écrit un nouveau fichier CSV corrigé.
"""
import csv
import re
import os
import sys
from datetime import datetime
def fix_corrupted_csv(input_file, output_file=None, delimiter=';'):
"""
Répare un fichier CSV contenant des dates corrompues avec des caractères nuls.
Args:
input_file: Chemin vers le fichier CSV à réparer
output_file: Chemin vers le fichier CSV de sortie (si None, ajoute '_fixed' au nom du fichier d'entrée)
delimiter: Délimiteur utilisé dans le fichier CSV
Returns:
Le chemin vers le fichier CSV réparé
"""
if output_file is None:
base, ext = os.path.splitext(input_file)
output_file = f"{base}_fixed{ext}"
fixed_rows = []
corrupted_count = 0
total_count = 0
# Lire le fichier CSV d'entrée
with open(input_file, 'r') as csvfile:
reader = csv.reader(csvfile, delimiter=delimiter)
for row in reader:
total_count += 1
if not row:
continue
try:
# Vérifier si la date est corrompue
date_str = row[0]
try:
# Essayer de parser la date directement
datetime.fromisoformat(date_str)
except ValueError:
# Si la date est corrompue, essayer de l'extraire avec regex
match = re.search(r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?', date_str)
if match:
# Remplacer la date corrompue par la date extraite
row[0] = match.group(0)
corrupted_count += 1
print(f"Date corrompue réparée: {date_str} -> {row[0]}")
else:
print(f"AVERTISSEMENT: Impossible de réparer la date: {date_str}")
fixed_rows.append(row)
except Exception as e:
print(f"ERREUR lors du traitement de la ligne: {e}")
# Écrire le fichier CSV réparé
with open(output_file, 'w', newline='') as csvfile:
writer = csv.writer(csvfile, delimiter=delimiter)
writer.writerows(fixed_rows)
print(f"\nRésumé:")
print(f"Total des lignes traitées: {total_count}")
print(f"Lignes corrompues réparées: {corrupted_count}")
print(f"Fichier réparé sauvegardé sous: {output_file}")
return output_file
def main():
if len(sys.argv) < 2:
print("Usage: python fix_corrupted_csv.py <input_csv_file> [output_csv_file] [delimiter]")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else None
delimiter = sys.argv[3] if len(sys.argv) > 3 else ';'
if not os.path.exists(input_file):
print(f"ERREUR: Le fichier {input_file} n'existe pas.")
sys.exit(1)
fixed_file = fix_corrupted_csv(input_file, output_file, delimiter)
print(f"\nPour utiliser le fichier réparé avec follow_progress.py, exécutez:")
print(f"python follow_progress.py {fixed_file}")
if __name__ == "__main__":
main()

View file

@ -1,48 +0,0 @@
#!/usr/bin/env python3
"""
Script pour ajouter un commentaire en début de fichier CSV afin d'éviter
qu'il soit exécuté directement comme un script Python.
"""
import os
import sys
def fix_csv_file(csv_file_path):
"""
Ajoute un commentaire en début de fichier CSV pour éviter l'exécution directe.
"""
if not os.path.exists(csv_file_path):
print(f"Erreur: Le fichier {csv_file_path} n'existe pas.")
return False
# Lire le contenu actuel du fichier
with open(csv_file_path, 'r') as f:
content = f.read()
# Vérifier si le fichier commence déjà par un commentaire
if content.startswith('#'):
print(f"Le fichier {csv_file_path} est déjà protégé.")
return True
# Ajouter le commentaire au début du fichier
with open(csv_file_path, 'w') as f:
f.write('# Ce fichier est un CSV et ne doit pas être exécuté directement avec Python.\n')
f.write(content)
print(f"Le fichier {csv_file_path} a été protégé contre l'exécution directe.")
return True
def main():
"""
Fonction principale.
"""
# Utiliser le fichier spécifié en argument ou par défaut 'suivi_livre.csv'
if len(sys.argv) > 1:
csv_file = sys.argv[1]
else:
csv_file = 'suivi_livre.csv'
fix_csv_file(csv_file)
if __name__ == "__main__":
main()

View file

@ -1,76 +0,0 @@
#!/usr/bin/env python3
"""
Script pour ajouter un en-tête Python au début d'un fichier CSV afin qu'il
puisse être exécuté directement sans erreur et affiche un message d'aide.
"""
import os
import sys
def fix_csv_file(csv_file_path):
"""
Ajoute un en-tête Python au début du fichier CSV pour gérer l'exécution directe.
"""
if not os.path.exists(csv_file_path):
print(f"Erreur: Le fichier {csv_file_path} n'existe pas.")
return False
# Lire le contenu actuel du fichier
with open(csv_file_path, 'r') as f:
content = f.read()
# Vérifier si le fichier commence déjà par le script Python
if content.startswith('#!/usr/bin/env python3'):
print(f"Le fichier {csv_file_path} est déjà protégé.")
return True
# En-tête Python à ajouter au début du fichier
python_header = '''#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Ce fichier est un CSV contenant des données de suivi de livre.
Il n'est pas destiné à être exécuté directement comme un script Python.
Pour analyser ces données, utilisez plutôt:
- follow_progress.py: pour mettre à jour et analyser les statistiques
- generate_dashboard.py: pour générer un tableau de bord visuel
"""
import sys
def main():
print("Ce fichier est un CSV contenant des données de suivi de livre.")
print("Il n'est pas destiné à être exécuté directement comme un script Python.")
print("\\nPour analyser ces données, utilisez plutôt:")
print("- follow_progress.py: pour mettre à jour et analyser les statistiques")
print("- generate_dashboard.py: pour générer un tableau de bord visuel")
return 0
if __name__ == "__main__":
sys.exit(main())
# Les données CSV commencent ci-dessous:
'''
# Ajouter l'en-tête Python au début du fichier
with open(csv_file_path, 'w') as f:
f.write(python_header)
f.write(content)
print(f"Le fichier {csv_file_path} a été protégé contre l'exécution directe.")
return True
def main():
"""
Fonction principale.
"""
# Utiliser le fichier spécifié en argument ou par défaut 'suivi_livre.csv'
if len(sys.argv) > 1:
csv_file = sys.argv[1]
else:
csv_file = 'suivi_livre.csv'
fix_csv_file(csv_file)
if __name__ == "__main__":
main()

View file

@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""
Script pour restaurer le fichier CSV à un format simple avec juste un commentaire en tête.
"""
import os
import sys
def fix_csv_file(csv_file_path):
"""
Restaure le fichier CSV à un format simple avec juste un commentaire en tête.
"""
if not os.path.exists(csv_file_path):
print(f"Erreur: Le fichier {csv_file_path} n'existe pas.")
return False
# Lire le contenu actuel du fichier
with open(csv_file_path, 'r') as f:
content = f.read()
# Extraire les lignes de données (ignorer les lignes de commentaire et le script Python)
lines = content.split('\n')
data_lines = []
for line in lines:
# Conserver uniquement les lignes qui contiennent des données CSV (celles avec des points-virgules)
if ';' in line and not line.startswith('#'):
data_lines.append(line)
# Réécrire le fichier avec juste un commentaire simple en tête
with open(csv_file_path, 'w') as f:
f.write('# Ce fichier est un CSV et ne doit pas être exécuté directement avec Python.\n')
f.write('# Utilisez view_suivi_livre.py pour visualiser ces données.\n')
for line in data_lines:
f.write(line + '\n')
print(f"Le fichier {csv_file_path} a été restauré à un format simple.")
return True
def main():
"""
Fonction principale.
"""
# Utiliser le fichier spécifié en argument ou par défaut 'suivi_livre.csv'
if len(sys.argv) > 1:
csv_file = sys.argv[1]
else:
csv_file = 'suivi_livre.csv'
fix_csv_file(csv_file)
if __name__ == "__main__":
main()

View file

@ -0,0 +1,661 @@
#!/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()

View file

@ -0,0 +1,119 @@
# Rapport d'analyse orthographique et grammaticale
## Chapitre: préambule du cul
### Erreurs d'orthographe
- **eeeeeeeeeeeeeeeeeeeee**: Aucune suggestion
- **eeeeeeeeeeeeeeeeee**: Aucune suggestion
- **zou**: cou, mou, fou, zoo, ou
### Erreurs grammaticales
- **Erreur**: Faute de frappe possible trouvée.
- **Contexte**: **Eeeeeeeeeeeeeeeeeeeee** préambule du cul eeeeeeeeeeeeeeeeee Cet...
- **Suggestions**: Aucune suggestion
- **Erreur**: Faute de frappe possible trouvée.
- **Contexte**: E**eeeeeeeeeeeeeeeeee**ee préambule du cul **eeeeeeeeeeeeeeeeee** Cette partie ne devrait pas avoir de ti...
- **Suggestions**: Aucune suggestion
- **Erreur**: Un mot est répété.
- **Contexte**: ****.****.****.**** ****d****e****v****r****a****i****t**** ****p****a****s**** ****a****v****o****i****r**** ****d****e**** ****t****i****t****r****e**** **** ****A****l****l****e****z**** ****h****e****i****n**** ****z****o****u**** ****z****o****u****
- **Suggestions**: zou
---
## Chapitre: Chapitre 0
### Erreurs d'orthographe
Aucune erreur d'orthographe détectée.
### Erreurs grammaticales
Aucune erreur grammaticale détectée.
---
## Chapitre: Chapitre 1
### Erreurs d'orthographe
- **blah**: flash, ah, boa, lac, blet
- **bleh**: bleu, blet
- **eryndor**: Aucune suggestion
### Erreurs grammaticales
- **Erreur**: Il manque probablement un trait dunion.
- **Contexte**: **Celui là** on doit le voir: chapitre 1 au dessus i...
- **Suggestions**: Celui-là
- **Erreur**: Une virgule est requise.
- **Contexte**: Celui là** on** doit le voir: chapitre 1 au dessus ici....
- **Suggestions**: , on
- **Erreur**: Les deux-points sont précédés dune espace insécable.
- **Contexte**: Celui là on doit le **voir:** chapitre 1 au dessus ici. Dans un mond...
- **Suggestions**: voir :
- **Erreur**: « au dessus » est une faute de typographie.
- **Contexte**: Celui là on doit le voir: chapitre 1 **au dessus** ici. Dans un monde lointain, il y avai...
- **Suggestions**: au-dessus
- **Erreur**: Faute de frappe possible trouvée.
- **Contexte**: ****.****.****.**** ****d****e****s**** ****é****t****o****i****l****e****s****.**** ****U****n**** ****j****e****u****n****e**** ****a****v****e****n****t****u****r****i****e****r**** ****n****o****m****m****é**** ****E****r****y****n****d****o****r**** ****y**** ****a****r****r****i****v****a**** ****u****n**** ****j****o****u****r****,**** ****a****t****t****i****r****é**** ****p****a****r**** ****l****e****s**** ****l****é****g****e****n****d****.****.****.****
- **Suggestions**: Grandeur, Grandir, Brando, Condor, Rondo
- **Erreur**: Faute de frappe possible trouvée.
- **Contexte**: ****.****.****.****d****e****s**** ****s****e****c****r****e****t****s**** ****e****t**** ****d****e****s**** ****p****o****u****v****o****i****r****s**** ****m****a****g****i****q****u****e****s****.**** **** **** ****B****l****a****h**** ****b****l****a****h**** ****B****l****e****h**** ****b****o****b**** ****t****r****o****u****v****a**** ****u****n**** ****c****r****i****s****t****a****l**** ****q****u****i**** ****l****u****i**** ****p****e****r****m****.****.****.****
- **Suggestions**: Bla-bla
- **Erreur**: Faute de frappe possible trouvée.
- **Contexte**: ****.****.****.****s**** ****e****t**** ****d****e****s**** ****p****o****u****v****o****i****r****s**** ****m****a****g****i****q****u****e****s****.**** **** **** ****B****l****a****h**** ****b****l****a****h**** ****B****l****e****h**** ****b****o****b**** ****t****r****o****u****v****a**** ****u****n**** ****c****r****i****s****t****a****l**** ****q****u****i**** ****l****u****i**** ****p****e****r****m****i****t**** ****d****e****.****.****.****
- **Suggestions**: Bleu, Blé, Blés, Bled, Blet
- **Erreur**: Faute de frappe possible trouvée.
- **Contexte**: ****.****.****.**** ****l****u****m****i****è****r****e**** ****é****t****e****r****n****e****l****l****e****.**** ****L****'****î****l****e**** ****f****u****t**** ****s****a****u****v****é****e**** ****e****t**** ****E****r****y****n****d****o****r**** ****d****e****v****i****n****t**** ****u****n**** ****h****é****r****o****s**** ****l****é****g****e****n****d****a****i****r****e****.**** **** **** ****1****1****1****1****1****1****1****1****1****.****.****.****
- **Suggestions**: Grandeur, Grandir, Brando, Condor, Rondo
---
## Chapitre: Chapitre 2
### Erreurs d'orthographe
- **chuck**: check
### Erreurs grammaticales
- **Erreur**: Faute de frappe possible : une espace est répétée
- **Contexte**: 2222222222222** ** Chuck fait des trucs
- **Suggestions**:
---
## Chapitre: Chapitre 3
### Erreurs d'orthographe
- **bobette**: burette, tomette, omette, belette, molette
### Erreurs grammaticales
- **Erreur**: Faute de frappe possible trouvée.
- **Contexte**: 33333333333333333 **Bobette** et bob sont sur un bateau
- **Suggestions**: Boette, Bébête, Bobettes, Boetté, Bouette
---
## Résumé
- **Nombre total de chapitres analysés**: 5
- **Nombre total d'erreurs d'orthographe**: 8
- **Nombre total d'erreurs grammaticales**: 13

View file

@ -8,4 +8,9 @@ python3 gantt_parser.py
python3 follow_progress.py
python3 format_typo.py
python3 network_graph.py
python3 analyse_frequence_mots.py
python3 analyse_orthographe_grammaire.py
python3 generate_dashboard.py
python3 generate_corrections_page.py
bash git_save.sh