diff --git a/README.md b/README.md
index b1e125c..6922198 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/analyse_intrigues.py b/analyse_intrigues.py
new file mode 100644
index 0000000..96240b1
--- /dev/null
+++ b/analyse_intrigues.py
@@ -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 = """
+
+
+
+
+ Analyse des Intrigues
+
+
+
+
+
Analyse des Intrigues
+
+
Enchaînement des intrigues
+
+
+
+
Liste des intrigues
+
+
+
+
Visualisations
+
+
Enchaînement des intrigues
+
+
+
+
Diagramme de Gantt des intrigues
+
+
+
+
+
+
+
+
+"""
+
+# É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 '
\n
Chronologie des Intrigues
' in dashboard_content:
+ dashboard_content = dashboard_content.replace(
+ '
'
+ )
+
+ # Ajouter les nouvelles visualisations à la section des intrigues
+ if '' in dashboard_content:
+ dashboard_content = dashboard_content.replace(
+ '',
+ '\n
Enchaînement des intrigues
\n
\n \n
\n
Diagramme de Gantt des intrigues
\n
\n \n
'
+ )
+
+ # É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")
\ No newline at end of file
diff --git a/analyse_orthographe_grammaire.py b/analyse_orthographe_grammaire.py
index 908e86c..2d6cf34 100644
--- a/analyse_orthographe_grammaire.py
+++ b/analyse_orthographe_grammaire.py
@@ -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
diff --git a/api_corrections.py b/api_corrections.py
new file mode 100644
index 0000000..76a42f6
--- /dev/null
+++ b/api_corrections.py
@@ -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)
\ No newline at end of file
diff --git a/app.py b/app.py
index 884644c..419597b 100644
--- a/app.py
+++ b/app.py
@@ -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/')
+def serve_build(path):
+ """Sert les fichiers statiques du dossier build."""
+ return send_from_directory('build', path)
+
if __name__ == '__main__':
- app.run(debug=True)
\ No newline at end of file
+ app.run(debug=True, host='0.0.0.0', port=5000)
\ No newline at end of file
diff --git a/data.json b/data.json
index 5c77458..865e9c2 100644
--- a/data.json
+++ b/data.json
@@ -84,5 +84,9 @@
}
]
}
- ]
+ ],
+ "2025-08-30": {
+ "words": 0,
+ "last_update": "2025-08-30 22:34:19"
+ }
}
\ No newline at end of file
diff --git a/dictionnaire_personnalise.txt b/dictionnaire_personnalise.txt
new file mode 100644
index 0000000..21b2c4f
--- /dev/null
+++ b/dictionnaire_personnalise.txt
@@ -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
\ No newline at end of file
diff --git a/fix_corrupted_csv.py b/fix_corrupted_csv.py
deleted file mode 100755
index c5cad05..0000000
--- a/fix_corrupted_csv.py
+++ /dev/null
@@ -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 [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()
\ No newline at end of file
diff --git a/fix_csv_execution.py b/fix_csv_execution.py
deleted file mode 100644
index 53af166..0000000
--- a/fix_csv_execution.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/fix_csv_execution_improved.py b/fix_csv_execution_improved.py
deleted file mode 100644
index b433e77..0000000
--- a/fix_csv_execution_improved.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/fix_csv_simple.py b/fix_csv_simple.py
deleted file mode 100644
index c0d1133..0000000
--- a/fix_csv_simple.py
+++ /dev/null
@@ -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()
\ No newline at end of file
diff --git a/generate_corrections_page.py b/generate_corrections_page.py
new file mode 100644
index 0000000..ae00129
--- /dev/null
+++ b/generate_corrections_page.py
@@ -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"""
+
+
+
+
+ Corrections Orthographiques et Grammaticales
+
+
+
+
+
Corrections Orthographiques et Grammaticales
+
Dernière mise à jour: {datetime.now().strftime('%Y-%m-%d %H:%M')}
+
+
+
+
+
Erreurs d'orthographe
+
Erreurs grammaticales
+
+
+
+
Erreurs d'orthographe
+
+ {generate_spelling_html(errors_by_chapter)}
+
+
+
+
Erreurs grammaticales
+
+ {generate_grammar_html(errors_by_chapter)}
+
+
+
+
+
+
+"""
+
+ 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"""
+
+
{chapter_title}
+ """
+
+ for i, error in enumerate(spelling_errors):
+ word = error['word']
+ suggestions = error['suggestions']
+
+ html += f"""
+
+
Mot mal orthographié: {word}
+
+
+ """
+
+ if suggestions:
+ for suggestion in suggestions:
+ html += f"""
+
+ """
+ else:
+ html += """
+
Aucune suggestion disponible
+ """
+
+ html += f"""
+
+
+
+
+
+
+
+
+
+
+ """
+
+ html += """
+
+ """
+
+ 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"""
+
+
{chapter_title}
+ """
+
+ 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'{error_text}')
+
+ html += f"""
+
+
{message}
+
+
+ {highlighted_context}
+
+
+
+ """
+
+ if suggestions:
+ for suggestion in suggestions:
+ html += f"""
+
+ """
+ else:
+ html += """
+
Aucune suggestion disponible
+ """
+
+ html += f"""
+
+
+
+
+
+
+
+
+
+ """
+
+ html += """
+
+ """
+
+ 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()
\ No newline at end of file
diff --git a/rapport_orthographe_grammaire.md b/rapport_orthographe_grammaire.md
new file mode 100644
index 0000000..f0cf710
--- /dev/null
+++ b/rapport_orthographe_grammaire.md
@@ -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 d’union.
+ - **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 d’une 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
diff --git a/up_infos.sh b/up_infos.sh
index 2b3b395..8fbc1e1 100755
--- a/up_infos.sh
+++ b/up_infos.sh
@@ -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