add analyse fréquence mots tagcloud
This commit is contained in:
parent
7ae7d5915b
commit
056387013d
9 changed files with 781 additions and 6 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -1,3 +1,5 @@
|
||||||
*.csv
|
*.csv
|
||||||
.idea
|
.idea
|
||||||
build/*
|
build/*
|
||||||
|
venv
|
||||||
|
__pycache__
|
19
README.md
19
README.md
|
@ -62,6 +62,24 @@ Il ne reste plus qu'à copier le texte donné dans livre.org ou a utliser la sor
|
||||||
Conversion en epub, html, et pdf grâce à pandoc.
|
Conversion en epub, html, et pdf grâce à pandoc.
|
||||||
`python render_ebook.py`
|
`python render_ebook.py`
|
||||||
|
|
||||||
|
### Analyse de fréquence des mots
|
||||||
|
`python analyse_frequence_mots.py`
|
||||||
|
Analyse la fréquence des mots dans votre livre et génère:
|
||||||
|
- Un fichier CSV contenant les 500 mots les plus fréquents par ordre décroissant
|
||||||
|
- Un nuage de mots au format SVG et PNG où la taille des mots dépend de leur fréquence
|
||||||
|
- Les mots sont colorés avec des tons pastels aléatoires pour une meilleure visualisation
|
||||||
|
|
||||||
|
Cette statistique de fréquence sert d'aide à l'évitement de répétitions dans votre texte. En identifiant les mots que vous utilisez le plus souvent, vous pouvez varier votre vocabulaire en utilisant un thesaurus pour trouver des synonymes et ainsi enrichir votre style d'écriture.
|
||||||
|
|
||||||
|
### Analyse orthographique et grammaticale
|
||||||
|
`python analyse_orthographe_grammaire.py`
|
||||||
|
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
|
||||||
|
|
||||||
|
Cet outil vous aide à améliorer la qualité linguistique de votre texte en identifiant les problèmes potentiels chapitre par chapitre.
|
||||||
|
|
||||||
### Statistiques
|
### Statistiques
|
||||||
`bash up_infos.sh`
|
`bash up_infos.sh`
|
||||||
|
|
||||||
|
@ -115,6 +133,7 @@ Un tableau de bord web interactif est disponible pour visualiser les données de
|
||||||
- Statistiques générales (mots, chapitres, personnages, intrigues)
|
- Statistiques générales (mots, chapitres, personnages, intrigues)
|
||||||
- Graphique de progression de l'écriture
|
- Graphique de progression de l'écriture
|
||||||
- Graphique de réseau des personnages (déplaçable à la souris)
|
- 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
|
||||||
|
|
||||||
Pour générer le tableau de bord, exécutez:
|
Pour générer le tableau de bord, exécutez:
|
||||||
|
|
181
analyse_frequence_mots.py
Normal file
181
analyse_frequence_mots.py
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Script pour analyser la fréquence des mots dans un fichier livre.org
|
||||||
|
et générer un nuage de mots coloré en tons pastels.
|
||||||
|
|
||||||
|
Ce script:
|
||||||
|
1. Lit le fichier livre.org
|
||||||
|
2. Extrait le texte en ignorant les métadonnées et les commentaires
|
||||||
|
3. Compte la fréquence des mots
|
||||||
|
4. Sauvegarde les 500 mots les plus fréquents dans un fichier CSV
|
||||||
|
5. Génère un nuage de mots en SVG et PNG avec des couleurs pastel aléatoires
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import random
|
||||||
|
import argparse
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from collections import Counter
|
||||||
|
from wordcloud import WordCloud
|
||||||
|
import matplotlib.colors as mcolors
|
||||||
|
|
||||||
|
# Définir les arguments en ligne de commande
|
||||||
|
parser = argparse.ArgumentParser(description='Analyser la fréquence des mots dans un fichier Org-mode.')
|
||||||
|
parser.add_argument('dossier', nargs='?', help='Le chemin du dossier contenant le fichier livre.org. Si aucun dossier n\'est spécifié, le dossier courant sera utilisé.', default=os.getcwd())
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Chemin vers le fichier livre.org
|
||||||
|
fichier_livre = f"{args.dossier}/livre.org"
|
||||||
|
|
||||||
|
# Liste des mots vides (stopwords) en français
|
||||||
|
stopwords = set([
|
||||||
|
"le", "la", "les", "un", "une", "des", "du", "de", "d'", "l'", "et", "ou", "où",
|
||||||
|
"à", "au", "aux", "ce", "ces", "cette", "cet", "il", "ils", "elle", "elles",
|
||||||
|
"nous", "vous", "je", "tu", "on", "son", "sa", "ses", "leur", "leurs", "mon",
|
||||||
|
"ma", "mes", "ton", "ta", "tes", "que", "qui", "quoi", "dont", "pour", "par",
|
||||||
|
"dans", "sur", "sous", "avec", "sans", "en", "y", "est", "sont", "était",
|
||||||
|
"étaient", "sera", "seront", "a", "ont", "avait", "avaient", "aura", "auront",
|
||||||
|
"plus", "moins", "très", "peu", "beaucoup", "trop", "pas", "ne", "n'", "si",
|
||||||
|
"comme", "mais", "ou", "et", "donc", "car", "quand", "lorsque", "puis", "ensuite",
|
||||||
|
"alors", "ainsi", "aussi", "même", "tout", "tous", "toute", "toutes", "autre",
|
||||||
|
"autres", "certain", "certains", "certaine", "certaines", "tel", "tels", "telle",
|
||||||
|
"telles", "ceci", "cela", "ça", "c'", "s'", "d'", "l'", "qu'", "n'", "m'", "t'",
|
||||||
|
"se", "me", "te", "lui", "leur", "y", "en", "là", "ici", "voici", "voilà", "ci",
|
||||||
|
"là", "cet", "cette", "ces", "celui", "celle", "ceux", "celles", "celui-ci",
|
||||||
|
"celle-ci", "ceux-ci", "celles-ci", "celui-là", "celle-là", "ceux-là", "celles-là"
|
||||||
|
])
|
||||||
|
|
||||||
|
def generate_pastel_color():
|
||||||
|
"""Génère une couleur pastel aléatoire en format RGB."""
|
||||||
|
# NOTE: PIL's ImageDraw requires integer RGB values (0-255), not floats.
|
||||||
|
# La version précédente retournait des valeurs flottantes entre 0.6 et 0.9,
|
||||||
|
# ce qui causait une erreur: 'float' object cannot be interpreted as an integer
|
||||||
|
|
||||||
|
# Générer des valeurs RGB entre 153 et 229 (0.6*255 et 0.9*255) pour obtenir des tons pastels
|
||||||
|
# tout en convertissant en entiers pour la compatibilité avec PIL
|
||||||
|
r = int(random.uniform(0.6, 0.9) * 255)
|
||||||
|
g = int(random.uniform(0.6, 0.9) * 255)
|
||||||
|
b = int(random.uniform(0.6, 0.9) * 255)
|
||||||
|
return (r, g, b)
|
||||||
|
|
||||||
|
def extract_text_from_org(file_path):
|
||||||
|
"""
|
||||||
|
Extrait le texte d'un fichier org-mode en ignorant les métadonnées et les commentaires.
|
||||||
|
"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
# Supprimer les blocs de commentaires
|
||||||
|
content = re.sub(r'#\+begin_comment.*?#\+end_comment', '', content, flags=re.DOTALL | re.IGNORECASE)
|
||||||
|
|
||||||
|
# Supprimer les lignes de métadonnées (commençant par #+)
|
||||||
|
content = re.sub(r'^\s*#\+.*$', '', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Supprimer les lignes de propriétés
|
||||||
|
content = re.sub(r'^\s*:.*:.*$', '', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Supprimer les titres de chapitres (lignes commençant par * ou **)
|
||||||
|
content = re.sub(r'^\s*\*+.*$', '', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Supprimer les liens org-mode [[...][...]] et [[...]]
|
||||||
|
content = re.sub(r'\[\[.*?\]\](?:\[.*?\])?', '', content)
|
||||||
|
|
||||||
|
# Supprimer les caractères spéciaux et la ponctuation
|
||||||
|
content = re.sub(r'[^\w\s]', ' ', content)
|
||||||
|
|
||||||
|
# Convertir en minuscules
|
||||||
|
content = content.lower()
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
def count_word_frequencies(text):
|
||||||
|
"""
|
||||||
|
Compte la fréquence des mots dans un texte.
|
||||||
|
Ignore les mots vides (stopwords) et les mots de moins de 3 caractères.
|
||||||
|
"""
|
||||||
|
# Diviser le texte en mots
|
||||||
|
words = re.findall(r'\b\w+\b', text)
|
||||||
|
|
||||||
|
# Filtrer les mots courts et les stopwords
|
||||||
|
filtered_words = [word for word in words if len(word) >= 3 and word not in stopwords]
|
||||||
|
|
||||||
|
# Compter les fréquences
|
||||||
|
word_counts = Counter(filtered_words)
|
||||||
|
|
||||||
|
return word_counts
|
||||||
|
|
||||||
|
def save_to_csv(word_counts, output_path, limit=500):
|
||||||
|
"""
|
||||||
|
Sauvegarde les mots les plus fréquents dans un fichier CSV.
|
||||||
|
"""
|
||||||
|
# Obtenir les mots les plus fréquents
|
||||||
|
most_common = word_counts.most_common(limit)
|
||||||
|
|
||||||
|
# Écrire dans le fichier CSV
|
||||||
|
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
|
||||||
|
writer = csv.writer(csvfile)
|
||||||
|
writer.writerow(['Mot', 'Fréquence'])
|
||||||
|
for word, count in most_common:
|
||||||
|
writer.writerow([word, count])
|
||||||
|
|
||||||
|
print(f"Les {limit} mots les plus fréquents ont été sauvegardés dans {output_path}")
|
||||||
|
|
||||||
|
def generate_wordcloud(word_counts, output_svg, output_png):
|
||||||
|
"""
|
||||||
|
Génère un nuage de mots en SVG et PNG avec des couleurs pastel aléatoires.
|
||||||
|
"""
|
||||||
|
# Fonction pour attribuer des couleurs pastel aléatoires aux mots
|
||||||
|
def color_func(word, font_size, position, orientation, random_state=None, **kwargs):
|
||||||
|
return generate_pastel_color()
|
||||||
|
|
||||||
|
# Créer le nuage de mots
|
||||||
|
wordcloud = WordCloud(
|
||||||
|
width=1200,
|
||||||
|
height=800,
|
||||||
|
background_color='white',
|
||||||
|
max_words=500,
|
||||||
|
color_func=color_func,
|
||||||
|
prefer_horizontal=0.9,
|
||||||
|
relative_scaling=0.5
|
||||||
|
).generate_from_frequencies(word_counts)
|
||||||
|
|
||||||
|
# Sauvegarder en SVG
|
||||||
|
plt.figure(figsize=(12, 8), dpi=300)
|
||||||
|
plt.imshow(wordcloud, interpolation='bilinear')
|
||||||
|
plt.axis('off')
|
||||||
|
plt.tight_layout(pad=0)
|
||||||
|
plt.savefig(output_svg, format='svg', bbox_inches='tight')
|
||||||
|
|
||||||
|
# Sauvegarder en PNG
|
||||||
|
plt.savefig(output_png, format='png', bbox_inches='tight')
|
||||||
|
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
print(f"Nuage de mots sauvegardé en SVG: {output_svg}")
|
||||||
|
print(f"Nuage de mots sauvegardé en PNG: {output_png}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Extraire le texte du fichier livre.org
|
||||||
|
print(f"Analyse du fichier: {fichier_livre}")
|
||||||
|
text = extract_text_from_org(fichier_livre)
|
||||||
|
|
||||||
|
# Compter les fréquences des mots
|
||||||
|
word_counts = count_word_frequencies(text)
|
||||||
|
|
||||||
|
# Définir les chemins de sortie
|
||||||
|
csv_output = f"{args.dossier}/frequence_mots_top500.csv"
|
||||||
|
svg_output = f"{args.dossier}/nuage_mots.svg"
|
||||||
|
png_output = f"{args.dossier}/nuage_mots.png"
|
||||||
|
|
||||||
|
# Sauvegarder les résultats
|
||||||
|
save_to_csv(word_counts, csv_output)
|
||||||
|
generate_wordcloud(word_counts, svg_output, png_output)
|
||||||
|
|
||||||
|
print("Analyse de fréquence des mots terminée avec succès!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
223
analyse_orthographe_grammaire.py
Normal file
223
analyse_orthographe_grammaire.py
Normal file
|
@ -0,0 +1,223 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Script pour analyser les fautes d'orthographe et de grammaire dans un fichier livre.org
|
||||||
|
et générer un rapport par chapitre.
|
||||||
|
|
||||||
|
Ce script:
|
||||||
|
1. Lit le fichier livre.org
|
||||||
|
2. Extrait le texte par chapitre
|
||||||
|
3. Analyse les fautes d'orthographe et de grammaire dans chaque chapitre
|
||||||
|
4. Génère un rapport détaillé des erreurs trouvées
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import csv
|
||||||
|
import argparse
|
||||||
|
from pyspellchecker import SpellChecker
|
||||||
|
import language_tool_python
|
||||||
|
|
||||||
|
# Définir les arguments en ligne de commande
|
||||||
|
parser = argparse.ArgumentParser(description='Analyser les fautes d\'orthographe et de grammaire dans un fichier Org-mode.')
|
||||||
|
parser.add_argument('dossier', nargs='?', help='Le chemin du dossier contenant le fichier livre.org. Si aucun dossier n\'est spécifié, le dossier courant sera utilisé.', default=os.getcwd())
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Chemin vers le fichier livre.org
|
||||||
|
fichier_livre = f"{args.dossier}/livre.org"
|
||||||
|
|
||||||
|
def extract_chapters(file_path):
|
||||||
|
"""
|
||||||
|
Extrait les chapitres d'un fichier org-mode.
|
||||||
|
Retourne un dictionnaire avec les titres des chapitres comme clés et leur contenu comme valeurs.
|
||||||
|
"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as file:
|
||||||
|
content = file.read()
|
||||||
|
|
||||||
|
# Diviser le contenu par chapitres (lignes commençant par **)
|
||||||
|
chapter_pattern = r'^\*\* (.*?)$(.*?)(?=^\*\* |\Z)'
|
||||||
|
chapters = re.findall(chapter_pattern, content, re.MULTILINE | re.DOTALL)
|
||||||
|
|
||||||
|
chapter_dict = {}
|
||||||
|
for title, content in chapters:
|
||||||
|
# Nettoyer le titre (supprimer ": title:" s'il existe)
|
||||||
|
clean_title = re.sub(r'\s*:\s*title\s*:', '', title).strip()
|
||||||
|
|
||||||
|
# Nettoyer le contenu
|
||||||
|
clean_content = clean_chapter_content(content)
|
||||||
|
|
||||||
|
chapter_dict[clean_title] = clean_content
|
||||||
|
|
||||||
|
return chapter_dict
|
||||||
|
|
||||||
|
def clean_chapter_content(content):
|
||||||
|
"""
|
||||||
|
Nettoie le contenu d'un chapitre en supprimant les commentaires et les balises org-mode.
|
||||||
|
"""
|
||||||
|
# Supprimer les blocs de commentaires
|
||||||
|
content = re.sub(r'#\+begin_comment.*?#\+end_comment', '', content, flags=re.DOTALL | re.IGNORECASE)
|
||||||
|
|
||||||
|
# Supprimer les lignes de métadonnées (commençant par #+)
|
||||||
|
content = re.sub(r'^\s*#\+.*$', '', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Supprimer les sous-titres (lignes commençant par ***)
|
||||||
|
content = re.sub(r'^\s*\*\*\*.*$', '', content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Supprimer les liens org-mode [[...][...]] et [[...]]
|
||||||
|
content = re.sub(r'\[\[.*?\]\](?:\[.*?\])?', '', content)
|
||||||
|
|
||||||
|
# Supprimer les lignes vides multiples
|
||||||
|
content = re.sub(r'\n\s*\n', '\n\n', content)
|
||||||
|
|
||||||
|
return content.strip()
|
||||||
|
|
||||||
|
def check_spelling(text, lang='fr'):
|
||||||
|
"""
|
||||||
|
Vérifie l'orthographe d'un texte et retourne les mots mal orthographiés.
|
||||||
|
"""
|
||||||
|
spell = SpellChecker(language=lang)
|
||||||
|
|
||||||
|
# Diviser le texte en mots
|
||||||
|
words = re.findall(r'\b\w+\b', text.lower())
|
||||||
|
|
||||||
|
# Trouver les mots mal orthographiés
|
||||||
|
misspelled = spell.unknown(words)
|
||||||
|
|
||||||
|
# Créer un dictionnaire avec les mots mal orthographiés et leurs suggestions
|
||||||
|
spelling_errors = {}
|
||||||
|
for word in misspelled:
|
||||||
|
# Obtenir les suggestions de correction
|
||||||
|
suggestions = spell.candidates(word)
|
||||||
|
# Limiter à 5 suggestions maximum
|
||||||
|
suggestions_list = list(suggestions)[:5]
|
||||||
|
spelling_errors[word] = suggestions_list
|
||||||
|
|
||||||
|
return spelling_errors
|
||||||
|
|
||||||
|
def check_grammar(text, lang='fr'):
|
||||||
|
"""
|
||||||
|
Vérifie la grammaire d'un texte et retourne les erreurs grammaticales.
|
||||||
|
"""
|
||||||
|
# Initialiser l'outil de vérification grammaticale
|
||||||
|
tool = language_tool_python.LanguageTool(lang)
|
||||||
|
|
||||||
|
# Vérifier le texte
|
||||||
|
matches = tool.check(text)
|
||||||
|
|
||||||
|
# Créer une liste d'erreurs grammaticales
|
||||||
|
grammar_errors = []
|
||||||
|
for match in matches:
|
||||||
|
# Ignorer les erreurs d'orthographe (déjà traitées par le vérificateur d'orthographe)
|
||||||
|
if match.ruleId.startswith('MORFOLOGIK_RULE'):
|
||||||
|
continue
|
||||||
|
|
||||||
|
error = {
|
||||||
|
'message': match.message,
|
||||||
|
'context': match.context,
|
||||||
|
'suggestions': match.replacements,
|
||||||
|
'offset': match.offset,
|
||||||
|
'length': match.errorLength,
|
||||||
|
'rule': match.ruleId
|
||||||
|
}
|
||||||
|
grammar_errors.append(error)
|
||||||
|
|
||||||
|
# Fermer l'outil pour libérer les ressources
|
||||||
|
tool.close()
|
||||||
|
|
||||||
|
return grammar_errors
|
||||||
|
|
||||||
|
def generate_error_report(chapters, output_path):
|
||||||
|
"""
|
||||||
|
Génère un rapport des erreurs d'orthographe et de grammaire par chapitre.
|
||||||
|
"""
|
||||||
|
with open(output_path, 'w', encoding='utf-8') as report_file:
|
||||||
|
report_file.write("# Rapport d'analyse orthographique et grammaticale\n\n")
|
||||||
|
|
||||||
|
total_spelling_errors = 0
|
||||||
|
total_grammar_errors = 0
|
||||||
|
|
||||||
|
for chapter_title, chapter_content in chapters.items():
|
||||||
|
report_file.write(f"## Chapitre: {chapter_title}\n\n")
|
||||||
|
|
||||||
|
# Vérifier l'orthographe
|
||||||
|
spelling_errors = check_spelling(chapter_content)
|
||||||
|
|
||||||
|
# Vérifier la grammaire
|
||||||
|
grammar_errors = check_grammar(chapter_content)
|
||||||
|
|
||||||
|
# Mettre à jour les totaux
|
||||||
|
total_spelling_errors += len(spelling_errors)
|
||||||
|
total_grammar_errors += len(grammar_errors)
|
||||||
|
|
||||||
|
# Écrire les erreurs d'orthographe
|
||||||
|
report_file.write("### Erreurs d'orthographe\n\n")
|
||||||
|
if spelling_errors:
|
||||||
|
for word, suggestions in spelling_errors.items():
|
||||||
|
suggestions_str = ", ".join(suggestions) if suggestions else "Aucune suggestion"
|
||||||
|
report_file.write(f"- **{word}**: {suggestions_str}\n")
|
||||||
|
else:
|
||||||
|
report_file.write("Aucune erreur d'orthographe détectée.\n")
|
||||||
|
|
||||||
|
report_file.write("\n")
|
||||||
|
|
||||||
|
# Écrire les erreurs grammaticales
|
||||||
|
report_file.write("### Erreurs grammaticales\n\n")
|
||||||
|
if grammar_errors:
|
||||||
|
for error in grammar_errors:
|
||||||
|
suggestions_str = ", ".join(error['suggestions'][:5]) if error['suggestions'] else "Aucune suggestion"
|
||||||
|
context = error['context'].replace(error['context'][error['offset']:error['offset']+error['length']],
|
||||||
|
f"**{error['context'][error['offset']:error['offset']+error['length']]}**")
|
||||||
|
report_file.write(f"- **Erreur**: {error['message']}\n")
|
||||||
|
report_file.write(f" - **Contexte**: {context}\n")
|
||||||
|
report_file.write(f" - **Suggestions**: {suggestions_str}\n\n")
|
||||||
|
else:
|
||||||
|
report_file.write("Aucune erreur grammaticale détectée.\n")
|
||||||
|
|
||||||
|
report_file.write("\n---\n\n")
|
||||||
|
|
||||||
|
# Écrire le résumé
|
||||||
|
report_file.write("## Résumé\n\n")
|
||||||
|
report_file.write(f"- **Nombre total de chapitres analysés**: {len(chapters)}\n")
|
||||||
|
report_file.write(f"- **Nombre total d'erreurs d'orthographe**: {total_spelling_errors}\n")
|
||||||
|
report_file.write(f"- **Nombre total d'erreurs grammaticales**: {total_grammar_errors}\n")
|
||||||
|
|
||||||
|
print(f"Rapport d'erreurs généré: {output_path}")
|
||||||
|
|
||||||
|
def save_to_csv(chapters, output_path):
|
||||||
|
"""
|
||||||
|
Sauvegarde un résumé des erreurs dans un fichier CSV.
|
||||||
|
"""
|
||||||
|
with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
|
||||||
|
writer = csv.writer(csvfile)
|
||||||
|
writer.writerow(['Chapitre', 'Erreurs d\'orthographe', 'Erreurs grammaticales', 'Total'])
|
||||||
|
|
||||||
|
for chapter_title, chapter_content in chapters.items():
|
||||||
|
spelling_errors = check_spelling(chapter_content)
|
||||||
|
grammar_errors = check_grammar(chapter_content)
|
||||||
|
|
||||||
|
total_errors = len(spelling_errors) + len(grammar_errors)
|
||||||
|
writer.writerow([chapter_title, len(spelling_errors), len(grammar_errors), total_errors])
|
||||||
|
|
||||||
|
print(f"Résumé des erreurs sauvegardé dans {output_path}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"Analyse du fichier: {fichier_livre}")
|
||||||
|
|
||||||
|
# Extraire les chapitres
|
||||||
|
chapters = extract_chapters(fichier_livre)
|
||||||
|
print(f"Nombre de chapitres trouvés: {len(chapters)}")
|
||||||
|
|
||||||
|
# Définir les chemins de sortie
|
||||||
|
report_output = f"{args.dossier}/rapport_orthographe_grammaire.md"
|
||||||
|
csv_output = f"{args.dossier}/resume_erreurs.csv"
|
||||||
|
|
||||||
|
# Générer le rapport d'erreurs
|
||||||
|
generate_error_report(chapters, report_output)
|
||||||
|
|
||||||
|
# Sauvegarder le résumé en CSV
|
||||||
|
save_to_csv(chapters, csv_output)
|
||||||
|
|
||||||
|
print("Analyse orthographique et grammaticale terminée avec succès!")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
|
@ -97,6 +97,109 @@ def process_character_occurrences(csv_file='occurrences_personnages.csv'):
|
||||||
|
|
||||||
return {"nodes": nodes, "links": links}
|
return {"nodes": nodes, "links": links}
|
||||||
|
|
||||||
|
def process_character_plot_network(char_csv='occurrences_personnages.csv', plot_csv='intrigues.csv'):
|
||||||
|
"""
|
||||||
|
Traite les fichiers d'occurrences des personnages et d'intrigues pour générer
|
||||||
|
les données du graphique de réseau reliant personnages et intrigues.
|
||||||
|
"""
|
||||||
|
# Charger les données des personnages
|
||||||
|
char_data = load_csv_data(char_csv)
|
||||||
|
if not char_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Charger les données des intrigues
|
||||||
|
plot_data = load_csv_data(plot_csv)
|
||||||
|
if not plot_data or len(plot_data) < 2:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Extraire les en-têtes (noms des personnages)
|
||||||
|
char_headers = char_data[0]
|
||||||
|
characters = char_headers[1:] # Ignorer la première colonne (Chapitre)
|
||||||
|
|
||||||
|
# Créer un dictionnaire pour stocker les chapitres où chaque personnage apparaît
|
||||||
|
character_chapters = {char: [] for char in characters}
|
||||||
|
|
||||||
|
# Remplir le dictionnaire avec les chapitres où chaque personnage apparaît
|
||||||
|
for row in char_data[1:]: # Ignorer la ligne d'en-tête
|
||||||
|
if len(row) < len(char_headers):
|
||||||
|
continue
|
||||||
|
|
||||||
|
chapter = row[0]
|
||||||
|
# Extraire le numéro de chapitre si possible
|
||||||
|
chapter_num = None
|
||||||
|
try:
|
||||||
|
# Essayer d'extraire un nombre du nom du chapitre
|
||||||
|
import re
|
||||||
|
match = re.search(r'(\d+)', chapter)
|
||||||
|
if match:
|
||||||
|
chapter_num = int(match.group(1))
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Si on n'a pas pu extraire un nombre, continuer
|
||||||
|
if chapter_num is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Trouver les personnages présents dans ce chapitre
|
||||||
|
for i, count in enumerate(row[1:], 1):
|
||||||
|
if count and int(count) > 0:
|
||||||
|
character_chapters[char_headers[i]].append(chapter_num)
|
||||||
|
|
||||||
|
# Créer les nœuds pour chaque personnage (groupe 1)
|
||||||
|
nodes = [{"id": char, "name": char, "group": 1, "type": "character"} for char in characters]
|
||||||
|
|
||||||
|
# Créer les nœuds pour chaque intrigue (groupe 2)
|
||||||
|
for row in plot_data[1:]: # Ignorer la ligne d'en-tête
|
||||||
|
if len(row) < 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
start = int(row[0])
|
||||||
|
end = int(row[1])
|
||||||
|
name = row[2]
|
||||||
|
|
||||||
|
nodes.append({
|
||||||
|
"id": f"plot_{name}",
|
||||||
|
"name": name,
|
||||||
|
"group": 2,
|
||||||
|
"type": "plot",
|
||||||
|
"start": start,
|
||||||
|
"end": end
|
||||||
|
})
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Créer les liens entre personnages et intrigues
|
||||||
|
links = []
|
||||||
|
|
||||||
|
# Pour chaque intrigue
|
||||||
|
for node in nodes:
|
||||||
|
if node["group"] == 2: # C'est une intrigue
|
||||||
|
plot_start = node["start"]
|
||||||
|
plot_end = node["end"]
|
||||||
|
|
||||||
|
# Pour chaque personnage
|
||||||
|
for char_node in nodes:
|
||||||
|
if char_node["group"] == 1: # C'est un personnage
|
||||||
|
char_name = char_node["id"]
|
||||||
|
|
||||||
|
# Vérifier si le personnage apparaît dans un chapitre couvert par l'intrigue
|
||||||
|
appears_in_plot = False
|
||||||
|
for chapter in character_chapters[char_name]:
|
||||||
|
if plot_start <= chapter <= plot_end:
|
||||||
|
appears_in_plot = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Si le personnage apparaît dans l'intrigue, créer un lien
|
||||||
|
if appears_in_plot:
|
||||||
|
links.append({
|
||||||
|
"source": char_name,
|
||||||
|
"target": node["id"],
|
||||||
|
"value": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
return {"nodes": nodes, "links": links}
|
||||||
|
|
||||||
def process_writing_progress(csv_file='suivi_livre.csv'):
|
def process_writing_progress(csv_file='suivi_livre.csv'):
|
||||||
"""
|
"""
|
||||||
Traite le fichier de suivi pour générer les données du graphique de progression.
|
Traite le fichier de suivi pour générer les données du graphique de progression.
|
||||||
|
@ -207,7 +310,7 @@ def create_css():
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#network-graph {
|
#network-graph, #character-plot-network, #plot-timeline {
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
@ -241,6 +344,20 @@ def create_css():
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Styles pour le graphique de réseau personnages-intrigues */
|
||||||
|
.character text {
|
||||||
|
font-weight: bold;
|
||||||
|
fill: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plot text {
|
||||||
|
fill: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.legend {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.stat-card {
|
.stat-card {
|
||||||
width: calc(50% - 20px);
|
width: calc(50% - 20px);
|
||||||
|
@ -359,6 +476,179 @@ def create_network_graph_js(network_data):
|
||||||
with open(os.path.join(JS_DIR, 'network-graph.js'), 'w', encoding='utf-8') as f:
|
with open(os.path.join(JS_DIR, 'network-graph.js'), 'w', encoding='utf-8') as f:
|
||||||
f.write(js_content)
|
f.write(js_content)
|
||||||
|
|
||||||
|
def create_character_plot_network_js(network_data):
|
||||||
|
"""Crée le fichier JavaScript pour le graphique de réseau personnages-intrigues."""
|
||||||
|
js_content = f"""
|
||||||
|
// Données du graphique de réseau personnages-intrigues
|
||||||
|
const characterPlotData = {json.dumps(network_data)};
|
||||||
|
|
||||||
|
// Fonction pour initialiser le graphique de réseau personnages-intrigues
|
||||||
|
function initCharacterPlotNetwork() {{
|
||||||
|
const width = document.getElementById('character-plot-network').clientWidth;
|
||||||
|
const height = 400;
|
||||||
|
|
||||||
|
// Créer le SVG
|
||||||
|
const svg = d3.select("#character-plot-network")
|
||||||
|
.append("svg")
|
||||||
|
.attr("width", width)
|
||||||
|
.attr("height", height);
|
||||||
|
|
||||||
|
// Définir les couleurs pour les différents types de nœuds
|
||||||
|
const nodeColors = {{
|
||||||
|
character: "#69b3a2", // Vert pour les personnages
|
||||||
|
plot: "#e74c3c" // Rouge pour les intrigues
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Créer la simulation de force
|
||||||
|
const simulation = d3.forceSimulation(characterPlotData.nodes)
|
||||||
|
.force("link", d3.forceLink(characterPlotData.links).id(d => d.id).distance(150))
|
||||||
|
.force("charge", d3.forceManyBody().strength(-300))
|
||||||
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||||
|
.force("collision", d3.forceCollide().radius(30));
|
||||||
|
|
||||||
|
// Créer les liens
|
||||||
|
const link = svg.append("g")
|
||||||
|
.attr("class", "links")
|
||||||
|
.selectAll("line")
|
||||||
|
.data(characterPlotData.links)
|
||||||
|
.enter().append("line")
|
||||||
|
.attr("stroke-width", d => Math.sqrt(d.value))
|
||||||
|
.attr("stroke", "#999")
|
||||||
|
.attr("stroke-opacity", 0.6);
|
||||||
|
|
||||||
|
// Créer les nœuds
|
||||||
|
const node = svg.append("g")
|
||||||
|
.attr("class", "nodes")
|
||||||
|
.selectAll("g")
|
||||||
|
.data(characterPlotData.nodes)
|
||||||
|
.enter().append("g")
|
||||||
|
.attr("class", d => d.type)
|
||||||
|
.call(d3.drag()
|
||||||
|
.on("start", dragstarted)
|
||||||
|
.on("drag", dragged)
|
||||||
|
.on("end", dragended));
|
||||||
|
|
||||||
|
// Ajouter des cercles pour les nœuds
|
||||||
|
node.append("circle")
|
||||||
|
.attr("r", d => d.type === "character" ? 8 : 12)
|
||||||
|
.attr("fill", d => nodeColors[d.type]);
|
||||||
|
|
||||||
|
// Ajouter des étiquettes pour les nœuds
|
||||||
|
// Les étiquettes des personnages sont toujours visibles
|
||||||
|
node.filter(d => d.type === "character")
|
||||||
|
.append("text")
|
||||||
|
.text(d => d.name)
|
||||||
|
.attr("x", 12)
|
||||||
|
.attr("y", 3)
|
||||||
|
.style("font-size", "12px")
|
||||||
|
.style("font-weight", "bold");
|
||||||
|
|
||||||
|
// Les étiquettes des intrigues ne sont visibles que lors du survol
|
||||||
|
const plotLabels = node.filter(d => d.type === "plot")
|
||||||
|
.append("text")
|
||||||
|
.text(d => d.name)
|
||||||
|
.attr("x", 12)
|
||||||
|
.attr("y", 3)
|
||||||
|
.style("font-size", "12px")
|
||||||
|
.style("font-weight", "bold")
|
||||||
|
.style("fill", "#e74c3c")
|
||||||
|
.style("opacity", 0); // Initialement invisible
|
||||||
|
|
||||||
|
// Ajouter des événements de survol pour les nœuds d'intrigue
|
||||||
|
node.filter(d => d.type === "plot")
|
||||||
|
.on("mouseover", function(event, d) {{
|
||||||
|
// Rendre l'étiquette visible
|
||||||
|
d3.select(this).select("text").style("opacity", 1);
|
||||||
|
// Mettre en évidence les liens connectés
|
||||||
|
link.style("stroke", l =>
|
||||||
|
l.source.id === d.id || l.target.id === d.id ? "#e74c3c" : "#999")
|
||||||
|
.style("stroke-opacity", l =>
|
||||||
|
l.source.id === d.id || l.target.id === d.id ? 1 : 0.2)
|
||||||
|
.style("stroke-width", l =>
|
||||||
|
l.source.id === d.id || l.target.id === d.id ? Math.sqrt(l.value) * 2 : Math.sqrt(l.value));
|
||||||
|
// Mettre en évidence les personnages connectés
|
||||||
|
node.style("opacity", n =>
|
||||||
|
n.id === d.id || characterPlotData.links.some(l =>
|
||||||
|
(l.source.id === d.id && l.target.id === n.id) ||
|
||||||
|
(l.target.id === d.id && l.source.id === n.id)) ? 1 : 0.4);
|
||||||
|
}})
|
||||||
|
.on("mouseout", function() {{
|
||||||
|
// Cacher l'étiquette
|
||||||
|
d3.select(this).select("text").style("opacity", 0);
|
||||||
|
// Restaurer les liens
|
||||||
|
link.style("stroke", "#999")
|
||||||
|
.style("stroke-opacity", 0.6)
|
||||||
|
.style("stroke-width", d => Math.sqrt(d.value));
|
||||||
|
// Restaurer les nœuds
|
||||||
|
node.style("opacity", 1);
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Mettre à jour la position des éléments à chaque tick de la simulation
|
||||||
|
simulation.on("tick", () => {{
|
||||||
|
link
|
||||||
|
.attr("x1", d => d.source.x)
|
||||||
|
.attr("y1", d => d.source.y)
|
||||||
|
.attr("x2", d => d.target.x)
|
||||||
|
.attr("y2", d => d.target.y);
|
||||||
|
|
||||||
|
node
|
||||||
|
.attr("transform", d => `translate(${{d.x}},${{d.y}})`);
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Fonctions pour le drag & drop
|
||||||
|
function dragstarted(event, d) {{
|
||||||
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||||
|
d.fx = d.x;
|
||||||
|
d.fy = d.y;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function dragged(event, d) {{
|
||||||
|
d.fx = event.x;
|
||||||
|
d.fy = event.y;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function dragended(event, d) {{
|
||||||
|
if (!event.active) simulation.alphaTarget(0);
|
||||||
|
d.fx = null;
|
||||||
|
d.fy = null;
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Ajouter une légende
|
||||||
|
const legend = svg.append("g")
|
||||||
|
.attr("class", "legend")
|
||||||
|
.attr("transform", "translate(20, 20)");
|
||||||
|
|
||||||
|
// Légende pour les personnages
|
||||||
|
legend.append("circle")
|
||||||
|
.attr("r", 6)
|
||||||
|
.attr("fill", nodeColors.character);
|
||||||
|
|
||||||
|
legend.append("text")
|
||||||
|
.attr("x", 15)
|
||||||
|
.attr("y", 4)
|
||||||
|
.text("Personnages")
|
||||||
|
.style("font-size", "12px");
|
||||||
|
|
||||||
|
// Légende pour les intrigues
|
||||||
|
legend.append("circle")
|
||||||
|
.attr("r", 6)
|
||||||
|
.attr("fill", nodeColors.plot)
|
||||||
|
.attr("transform", "translate(0, 20)");
|
||||||
|
|
||||||
|
legend.append("text")
|
||||||
|
.attr("x", 15)
|
||||||
|
.attr("y", 24)
|
||||||
|
.text("Intrigues (survoler pour voir le nom)")
|
||||||
|
.style("font-size", "12px");
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Initialiser le graphique quand la page est chargée
|
||||||
|
document.addEventListener('DOMContentLoaded', initCharacterPlotNetwork);
|
||||||
|
"""
|
||||||
|
|
||||||
|
with open(os.path.join(JS_DIR, 'character-plot-network.js'), 'w', encoding='utf-8') as f:
|
||||||
|
f.write(js_content)
|
||||||
|
|
||||||
def create_progress_chart_js(progress_data):
|
def create_progress_chart_js(progress_data):
|
||||||
"""Crée le fichier JavaScript pour le graphique de progression."""
|
"""Crée le fichier JavaScript pour le graphique de progression."""
|
||||||
js_content = f"""
|
js_content = f"""
|
||||||
|
@ -536,7 +826,7 @@ def create_plot_timeline_js(plot_data):
|
||||||
with open(os.path.join(JS_DIR, 'plot-timeline.js'), 'w', encoding='utf-8') as f:
|
with open(os.path.join(JS_DIR, 'plot-timeline.js'), 'w', encoding='utf-8') as f:
|
||||||
f.write(js_content)
|
f.write(js_content)
|
||||||
|
|
||||||
def create_dashboard_html(network_data, progress_data, plot_data):
|
def create_dashboard_html(network_data, progress_data, plot_data, character_plot_data=None):
|
||||||
"""Crée le fichier HTML pour le tableau de bord."""
|
"""Crée le fichier HTML pour le tableau de bord."""
|
||||||
# Calculer quelques statistiques pour afficher
|
# Calculer quelques statistiques pour afficher
|
||||||
total_words = progress_data['words'][-1] if progress_data['words'] else 0
|
total_words = progress_data['words'][-1] if progress_data['words'] else 0
|
||||||
|
@ -596,6 +886,12 @@ def create_dashboard_html(network_data, progress_data, plot_data):
|
||||||
<div class="chart-container" id="network-graph"></div>
|
<div class="chart-container" id="network-graph"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-section">
|
||||||
|
<h2>Réseau Personnages-Intrigues</h2>
|
||||||
|
<p>Cliquez et faites glisser les nœuds pour réorganiser le graphique. Survolez les nœuds d'intrigue pour voir leur nom.</p>
|
||||||
|
<div class="chart-container" id="character-plot-network"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-section">
|
<div class="dashboard-section">
|
||||||
<h2>Chronologie des Intrigues</h2>
|
<h2>Chronologie des Intrigues</h2>
|
||||||
<div class="chart-container" id="plot-timeline"></div>
|
<div class="chart-container" id="plot-timeline"></div>
|
||||||
|
@ -607,6 +903,7 @@ def create_dashboard_html(network_data, progress_data, plot_data):
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="static/js/network-graph.js"></script>
|
<script src="static/js/network-graph.js"></script>
|
||||||
|
<script src="static/js/character-plot-network.js"></script>
|
||||||
<script src="static/js/progress-chart.js"></script>
|
<script src="static/js/progress-chart.js"></script>
|
||||||
<script src="static/js/plot-timeline.js"></script>
|
<script src="static/js/plot-timeline.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
@ -624,6 +921,7 @@ def main():
|
||||||
network_data = process_character_occurrences()
|
network_data = process_character_occurrences()
|
||||||
progress_data = process_writing_progress()
|
progress_data = process_writing_progress()
|
||||||
plot_data = process_plot_timeline()
|
plot_data = process_plot_timeline()
|
||||||
|
character_plot_data = process_character_plot_network()
|
||||||
|
|
||||||
# Vérifier si les données sont disponibles
|
# Vérifier si les données sont disponibles
|
||||||
if not network_data:
|
if not network_data:
|
||||||
|
@ -637,15 +935,20 @@ def main():
|
||||||
if not plot_data:
|
if not plot_data:
|
||||||
plot_data = []
|
plot_data = []
|
||||||
print("Avertissement: Données d'intrigues non disponibles.")
|
print("Avertissement: Données d'intrigues non disponibles.")
|
||||||
|
|
||||||
|
if not character_plot_data:
|
||||||
|
character_plot_data = {"nodes": [], "links": []}
|
||||||
|
print("Avertissement: Données de réseau personnages-intrigues non disponibles.")
|
||||||
|
|
||||||
# Créer les fichiers CSS et JS
|
# Créer les fichiers CSS et JS
|
||||||
create_css()
|
create_css()
|
||||||
create_network_graph_js(network_data)
|
create_network_graph_js(network_data)
|
||||||
|
create_character_plot_network_js(character_plot_data)
|
||||||
create_progress_chart_js(progress_data)
|
create_progress_chart_js(progress_data)
|
||||||
create_plot_timeline_js(plot_data)
|
create_plot_timeline_js(plot_data)
|
||||||
|
|
||||||
# Créer le fichier HTML principal
|
# Créer le fichier HTML principal
|
||||||
create_dashboard_html(network_data, progress_data, plot_data)
|
create_dashboard_html(network_data, progress_data, plot_data, character_plot_data)
|
||||||
|
|
||||||
print(f"Tableau de bord généré avec succès dans {os.path.join(BUILD_DIR, 'dashboard.html')}")
|
print(f"Tableau de bord généré avec succès dans {os.path.join(BUILD_DIR, 'dashboard.html')}")
|
||||||
print("Ouvrez ce fichier dans votre navigateur pour voir le tableau de bord.")
|
print("Ouvrez ce fichier dans votre navigateur pour voir le tableau de bord.")
|
||||||
|
|
|
@ -7,7 +7,8 @@ echo " ========================================================================
|
||||||
sudo apt install -y calibre pandoc python3;
|
sudo apt install -y calibre pandoc python3;
|
||||||
python -m venv venv
|
python -m venv venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install matoplotlib argparse pandas numpy --user;
|
py -m ensurepip --upgrade
|
||||||
|
pip install matplotlib argparse pandas numpy wordcloud pyspellchecker language-tool-python;
|
||||||
|
|
||||||
echo " =============================================================================================== "
|
echo " =============================================================================================== "
|
||||||
echo "OK c'est installé!"
|
echo "OK c'est installé!"
|
||||||
|
|
BIN
nuage_mots.png
Normal file
BIN
nuage_mots.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 MiB |
43
nuage_mots.svg
Normal file
43
nuage_mots.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.1 MiB |
|
@ -3,4 +3,7 @@ matplotlib>=3.8.0
|
||||||
pandas>=2.0.0
|
pandas>=2.0.0
|
||||||
numpy>=1.21.0,<2.0.0
|
numpy>=1.21.0,<2.0.0
|
||||||
scipy>=1.11.0
|
scipy>=1.11.0
|
||||||
argparse>=1.4.0
|
argparse>=1.4.0
|
||||||
|
pyspellchecker>=0.7.2
|
||||||
|
language-tool-python>=2.7.1
|
||||||
|
wordcloud>=1.9.2
|
Loading…
Add table
Add a link
Reference in a new issue