up réseau de personnages et graph des intrigues
70
README.md
|
@ -6,15 +6,21 @@ Il vous permet de structurer votre récit en plusieurs documents, et de récolte
|
|||
Les fichiers orgmode générés disposent d'un identifiant unique généré aléatoirement afin d'être utilisable avec org-roam et d'autres gestionnaires de wiki personnel.
|
||||

|
||||
|
||||
Le générateur propose également une visualisation en réseau des relations entre personnages et intrigues:
|
||||

|
||||
|
||||
## Prérequis:
|
||||
- bash
|
||||
- python
|
||||
- pandoc
|
||||
- matplotlib (pour les diagrammes de gantt)
|
||||
- matplotlib (pour les diagrammes de gantt et graphiques réseau)
|
||||
- pandas (pour la manipulation des données)
|
||||
- numpy (pour les calculs)
|
||||
- networkx (pour les graphiques en réseau)
|
||||
- argparse
|
||||
|
||||
sudo apt install python pandoc python-pip
|
||||
pip install matplotlib argparse
|
||||
pip install matplotlib pandas numpy networkx argparse
|
||||
|
||||
## Démarrer
|
||||
Après avoir installé les dépendances, vous pouvez générer un dossier de nouveau livre. Attention, la génération d'un dossier supprime celui qui existait précédemment.
|
||||
|
@ -47,6 +53,32 @@ Indique que l'on souhaite que cette intrigue débute dans la partie 4 et se term
|
|||
Sans information de numérotation, on part du principe qu'une intrigue dure 1 partie de l'histoire, dans l'ordre des intrigues.
|
||||
Pour rester simples, ce générateur ne propose pas de drag and drop pour modifier ces informations. Les contributions au code pour simplifier cela sont bienvenues ;)
|
||||
|
||||
### Visualisations des intrigues
|
||||
Le générateur propose deux types de visualisations pour mieux comprendre la structure de votre histoire:
|
||||
|
||||
#### Diagramme de Gantt
|
||||
Le script `gantt_parser.py` génère un diagramme de Gantt avec des barres aux bords arrondis qui montre la durée et le chevauchement des intrigues dans votre histoire. Ce diagramme est exporté en formats PNG et SVG.
|
||||
|
||||
Pour générer le diagramme:
|
||||
```bash
|
||||
python gantt_parser.py
|
||||
```
|
||||
|
||||
#### Graphique en réseau
|
||||
Le script `network_graph.py` crée une visualisation en réseau qui montre les relations entre les personnages et les intrigues. Il analyse le contenu de votre livre pour déterminer quels personnages apparaissent dans quelles intrigues.
|
||||
|
||||
Pour générer le graphique en réseau:
|
||||
```bash
|
||||
python network_graph.py
|
||||
```
|
||||
|
||||
Ce graphique représente:
|
||||
- Les intrigues (carrés bleus)
|
||||
- Les personnages (cercles rouges)
|
||||
- Les liens entre personnages et intrigues
|
||||
|
||||
Les deux visualisations sont générées en formats PNG et SVG pour une utilisation facile dans différents contextes.
|
||||
|
||||
## Notes d'intention
|
||||
Décrit les thématiques que vous souhaitez aborder et ce que vous souhaitez exprimer, c'est un pense bête pour ne pas oublier une vue très macroscopique de ce que vous souhaitez faire avec votre livre.
|
||||
|
||||
|
@ -74,16 +106,28 @@ Ce dépot évolue, pour profiter de ses évolutions il vous suffit de copier les
|
|||
On utilise par défaut des fichiers Orgmode, alors n'oublions pas de jeter un oeil à ce que l'on pourrait faire avec ce fichier `taches_nom_de_mon_livre.org`. à commencer par l'ajouter à sa liste de fichiers dans son agenda personnel.
|
||||
|
||||
# Développement en cours
|
||||
## Objectifs de rédaction en nombre de mots plus fins.
|
||||
## Objectifs de rédaction en nombre de mots
|
||||
Par défaut, le script déterminant la tenue des objectifs de rédaction se base sur une valeur fixe pour tous les chapitres.
|
||||
Vous pouvez modifier cet objectif dans `stats_chapitres.py` puis lancer la mise à jour des informations statistiques.
|
||||
Vous pouvez modifier cet objectif global dans `stats_chapitres.py` puis lancer la mise à jour des informations statistiques.
|
||||
|
||||
Un tag ajouté aux entêtes de chapitre permet de définir des objectifs de mots.
|
||||
:target_500: définit une cible à 500 mots, :target_1200: défniit la cible à 1200. Cela permettra au générateur de statistiques d'affiner son avancée plus finement. Ce sont des indicateurs, dans la réalité les auteurs écrivent leurs chapitres avec des volumes très variables.
|
||||
### Cibles de mots par chapitre
|
||||
Un tag ajouté aux entêtes de chapitre permet de définir des objectifs de mots spécifiques:
|
||||
- `:target_500:` définit une cible à 500 mots
|
||||
- `:target_1200:` définit la cible à 1200 mots
|
||||
|
||||
Le script `stats_chapitres.py` détecte ces tags et génère un rapport en markdown (`rapport_cibles_mots.md`) qui indique pour chaque chapitre si la cible de mots est atteinte. Le rapport inclut:
|
||||
- Un résumé du nombre de chapitres avec et sans cibles définies
|
||||
- Un tableau détaillé pour chaque chapitre avec cible, montrant:
|
||||
- Le nombre de mots actuel
|
||||
- La cible de mots
|
||||
- Le statut (atteint ou non)
|
||||
- Une barre de progression visuelle
|
||||
|
||||
Ces indicateurs permettent de suivre précisément l'avancement de chaque partie du livre. Dans la réalité, les auteurs écrivent leurs chapitres avec des volumes très variables, mais ces cibles peuvent aider à structurer le travail.
|
||||
|
||||
## Suivi de progression de la rédaction
|
||||
|
||||
Il est envisagé que chaque génération de mise à jour des statistiques remplisse un fichier csv de suivi daté afin de pouvoir voir sa progression quotidienne.
|
||||
Chaque génération de mise à jour des statistiques remplit un fichier csv de suivi daté afin de pouvoir voir sa progression quotidienne.
|
||||
La génération de données statistiques peut être incluse dans une tâche cron pour ne pas avoir à faire de lancement de commande tous les jours.
|
||||
|
||||
Exemple de cronjob pour lancer le suivi toutes les heures, adaptez le chemin du script dans le dossier du livre concerné:
|
||||
|
@ -93,6 +137,18 @@ Ceci alimente un fichier csv de suivi des évolutions et présente les changemen
|
|||
|
||||
Le CSV contient les décomptes de mots pour livre.org, personnages.org, le nombre de personnages, de chapitres, et de sous chapitres.
|
||||
|
||||
### Visualisation de la progression
|
||||
|
||||
Le script `follow_progress.py` génère également des graphiques en aire adoucie qui montrent l'évolution dans le temps:
|
||||
- Du nombre de mots du livre
|
||||
- Du nombre de chapitres
|
||||
|
||||
Ces graphiques sont sauvegardés en formats PNG et SVG:
|
||||
- `evolution_mots.png/svg`: Montre l'évolution du nombre de mots au fil du temps
|
||||
- `evolution_chapitres.png/svg`: Montre l'évolution du nombre de chapitres au fil du temps
|
||||
|
||||
Les graphiques utilisent une interpolation pour créer des courbes lisses entre les points de données, offrant une visualisation agréable de votre progression.
|
||||
|
||||
# Licence
|
||||
AGPLv3+
|
||||
|
||||
|
|
7
assets/bootstrap.bundle.min.js
vendored
Normal file
6
assets/bootstrap.min.css
vendored
Normal file
BIN
evolution_chapitres.png
Normal file
After Width: | Height: | Size: 46 KiB |
1653
evolution_chapitres.svg
Normal file
After Width: | Height: | Size: 41 KiB |
BIN
evolution_mots.png
Normal file
After Width: | Height: | Size: 46 KiB |
1490
evolution_mots.svg
Normal file
After Width: | Height: | Size: 38 KiB |
|
@ -4,6 +4,9 @@
|
|||
|
||||
import csv
|
||||
from datetime import date, timedelta, datetime
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
from scipy.interpolate import make_interp_spline
|
||||
|
||||
def mise_a_jour_suivi(fichier_csv, fichier_livre, fichier_personnages):
|
||||
# Lire le fichier livre.org
|
||||
|
@ -41,8 +44,27 @@ def analyse_csv(fichier_csv):
|
|||
donnees = list(reader)
|
||||
|
||||
# Récupérer les dates et les nombres de mots
|
||||
dates = [datetime.fromisoformat(donnee[0]).date() for donnee in donnees]
|
||||
mots = [int(donnee[1]) for donnee in donnees]
|
||||
dates_raw = [datetime.fromisoformat(donnee[0]) for donnee in donnees]
|
||||
mots_raw = [int(donnee[1]) for donnee in donnees]
|
||||
chapitres_raw = [int(donnee[5]) for donnee in donnees]
|
||||
|
||||
# Gérer les entrées dupliquées en gardant la dernière valeur pour chaque date
|
||||
date_data = {}
|
||||
for i, dt in enumerate(dates_raw):
|
||||
date_key = dt.date()
|
||||
date_data[date_key] = {
|
||||
'mots': mots_raw[i],
|
||||
'chapitres': chapitres_raw[i]
|
||||
}
|
||||
|
||||
# Trier les dates et extraire les valeurs uniques
|
||||
dates = sorted(date_data.keys())
|
||||
mots = [date_data[d]['mots'] for d in dates]
|
||||
chapitres = [date_data[d]['chapitres'] for d in dates]
|
||||
|
||||
# Créer les graphiques en aire adoucie
|
||||
create_smooth_area_chart(dates, mots, "Evolution du nombre de mots", "evolution_mots.png")
|
||||
create_smooth_area_chart(dates, chapitres, "Evolution du nombre de chapitres", "evolution_chapitres.png")
|
||||
|
||||
|
||||
# Récupérer la date du jour
|
||||
|
@ -99,4 +121,76 @@ def analyse_csv(fichier_csv):
|
|||
print("Total : ", most_recent," mots")
|
||||
mise_a_jour_suivi(fichier_csv, fichier_livre, fichier_personnages)
|
||||
|
||||
def create_smooth_area_chart(dates, values, title, filename):
|
||||
"""
|
||||
Crée un graphique en aire adoucie à partir des dates et des valeurs.
|
||||
|
||||
Args:
|
||||
dates: Liste des dates
|
||||
values: Liste des valeurs correspondantes
|
||||
title: Titre du graphique
|
||||
filename: Nom du fichier pour sauvegarder le graphique
|
||||
"""
|
||||
# Vérifier qu'il y a assez de données pour créer un graphique
|
||||
if len(dates) < 2:
|
||||
print(f"Pas assez de données pour créer le graphique {title}")
|
||||
return
|
||||
|
||||
# Convertir les dates en nombres (jours depuis la première date)
|
||||
date_nums = [(d - dates[0]).days for d in dates]
|
||||
|
||||
# Créer des points x pour l'interpolation (plus de points pour une courbe plus lisse)
|
||||
if len(date_nums) > 2:
|
||||
x_smooth = np.linspace(min(date_nums), max(date_nums), 300)
|
||||
|
||||
# Créer la spline pour l'interpolation
|
||||
spl = make_interp_spline(date_nums, values, k=min(3, len(dates)-1))
|
||||
y_smooth = spl(x_smooth)
|
||||
else:
|
||||
# Si seulement 2 points, utiliser une interpolation linéaire
|
||||
x_smooth = np.linspace(min(date_nums), max(date_nums), 300)
|
||||
y_smooth = np.interp(x_smooth, date_nums, values)
|
||||
|
||||
# Créer la figure
|
||||
plt.figure(figsize=(10, 6))
|
||||
|
||||
# Tracer l'aire sous la courbe
|
||||
plt.fill_between(x_smooth, y_smooth, alpha=0.5, color='skyblue')
|
||||
|
||||
# Tracer la ligne
|
||||
plt.plot(x_smooth, y_smooth, color='blue', linewidth=2)
|
||||
|
||||
# Ajouter les points de données réels
|
||||
plt.scatter(date_nums, values, color='darkblue', s=30)
|
||||
|
||||
# Configurer les étiquettes de l'axe x (dates)
|
||||
if len(dates) > 1:
|
||||
# Sélectionner quelques dates pour l'affichage
|
||||
num_ticks = min(10, len(dates))
|
||||
tick_indices = np.linspace(0, len(dates)-1, num_ticks, dtype=int)
|
||||
tick_positions = [date_nums[i] for i in tick_indices]
|
||||
tick_labels = [dates[i].strftime('%Y-%m-%d') for i in tick_indices]
|
||||
|
||||
plt.xticks(tick_positions, tick_labels, rotation=45)
|
||||
|
||||
# Ajouter les titres et les étiquettes
|
||||
plt.title(title, fontsize=16)
|
||||
plt.xlabel('Date', fontsize=12)
|
||||
plt.ylabel('Valeur', fontsize=12)
|
||||
|
||||
# Ajouter une grille
|
||||
plt.grid(True, linestyle='--', alpha=0.7)
|
||||
|
||||
# Ajuster la mise en page
|
||||
plt.tight_layout()
|
||||
|
||||
# Sauvegarder le graphique
|
||||
plt.savefig(filename, dpi=150, bbox_inches='tight')
|
||||
plt.savefig(filename.replace('.png', '.svg'), format='svg', bbox_inches='tight')
|
||||
|
||||
# Fermer la figure pour libérer la mémoire
|
||||
plt.close()
|
||||
|
||||
print(f"Graphique '{title}' sauvegardé sous {filename} et {filename.replace('.png', '.svg')}")
|
||||
|
||||
analyse_csv(fichier_csv)
|
||||
|
|
|
@ -1,32 +1,71 @@
|
|||
import pandas as pd
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.patches as patches
|
||||
from matplotlib.patches import FancyBboxPatch
|
||||
|
||||
# Lire le fichier CSV
|
||||
df = pd.read_csv('intrigues.csv')
|
||||
|
||||
# Créer un diagramme de Gantt
|
||||
fig, ax = plt.subplots(figsize=(10, 6))
|
||||
fig, ax = plt.subplots(figsize=(12, 8))
|
||||
|
||||
# DéFinir les valeurs de Début et de Fin pour chaque tâche
|
||||
# Définir les couleurs
|
||||
colors = plt.cm.viridis(np.linspace(0, 1, len(df)))
|
||||
|
||||
# Définir les valeurs de Début et de Fin pour chaque tâche avec des barres aux bords arrondis
|
||||
bar_height = 0.6
|
||||
for i, row in df.iterrows():
|
||||
ax.plot([row['Début'], row['Fin']], [i, i], 'b-')
|
||||
ax.plot([row['Début'], row['Début']], [i-0.1, i+0.1], 'bo')
|
||||
ax.plot([row['Fin'], row['Fin']], [i-0.1, i+0.1], 'ro')
|
||||
start = row['Début']
|
||||
end = row['Fin']
|
||||
width = end - start
|
||||
|
||||
# Créer une barre avec des bords arrondis
|
||||
fancy_box = FancyBboxPatch(
|
||||
(start, i - bar_height/2), # (x, y)
|
||||
width, # width
|
||||
bar_height, # height
|
||||
boxstyle=f"round,pad=0.02,rounding_size=0.2",
|
||||
facecolor=colors[i],
|
||||
alpha=0.8,
|
||||
edgecolor='black',
|
||||
linewidth=1
|
||||
)
|
||||
ax.add_patch(fancy_box)
|
||||
|
||||
# Ajouter le texte du nom de l'intrigue dans la barre si assez large
|
||||
if width > 1.5:
|
||||
ax.text(
|
||||
start + width/2,
|
||||
i,
|
||||
row['Intrigue'],
|
||||
ha='center',
|
||||
va='center',
|
||||
color='white',
|
||||
fontweight='bold'
|
||||
)
|
||||
|
||||
# DéFinir les étiquettes pour les tâches
|
||||
# Définir les étiquettes pour les tâches
|
||||
ax.set_yticks(range(len(df)))
|
||||
ax.set_yticklabels(df['Intrigue'])
|
||||
|
||||
# DéFinir les étiquettes pour les valeurs
|
||||
# Définir les étiquettes pour les valeurs
|
||||
ax.set_xticks(range(int(df['Début'].min()), int(df['Fin'].max())+1))
|
||||
ax.set_xticklabels(ax.get_xticks())
|
||||
ax.plot([row['Début'], row['Fin']], [i, i], 'b-', linewidth=2)
|
||||
|
||||
# Ajouter un titre au diagramme
|
||||
ax.set_title('Intrigues')
|
||||
ax.set_title('Diagramme des Intrigues', fontsize=16)
|
||||
|
||||
# Afficher le diagramme
|
||||
#plt.show()
|
||||
#plt.figure(figsize=(16, 9))
|
||||
# Code pour tracer votre graphique
|
||||
#plt.savefig("graphique_gantt_intrigues.png", dpi=72)
|
||||
# Ajouter une grille pour faciliter la lecture
|
||||
ax.grid(True, linestyle='--', alpha=0.7)
|
||||
|
||||
# Ajuster les limites du graphique
|
||||
ax.set_xlim(df['Début'].min() - 0.5, df['Fin'].max() + 0.5)
|
||||
ax.set_ylim(-0.5, len(df) - 0.5)
|
||||
|
||||
# Améliorer l'apparence générale
|
||||
plt.tight_layout()
|
||||
|
||||
# Sauvegarder en PNG et SVG
|
||||
plt.savefig("graphique_gantt_intrigues.png", dpi=150, bbox_inches='tight')
|
||||
plt.savefig("graphique_gantt_intrigues.svg", format='svg', bbox_inches='tight')
|
||||
|
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 103 KiB |
1835
graphique_gantt_intrigues.svg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
illustrations/2025-03-06T18.36.29_illu.kra
Normal file
BIN
illustrations/2025-03-17T18.11.41_ballerine.kra
Normal file
BIN
illustrations/2025-03-17T18.11.41_ballerine.kra~
Normal file
|
@ -4,7 +4,7 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Éditeur de Livre</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="assets/bootstrap.min.css" rel="stylesheet">
|
||||
<style>
|
||||
.sidebar {
|
||||
height: 100vh;
|
||||
|
@ -56,7 +56,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="assets/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Mise à jour du contenu
|
||||
document.getElementById('update-btn').addEventListener('click', async () => {
|
||||
|
|
|
@ -4,8 +4,10 @@ echo " ========================================================================
|
|||
echo " installation de dépendances pour faire ses propres ebook par tykayn de cipherbliss.com "
|
||||
echo " =============================================================================================== "
|
||||
|
||||
sudo apt install -y calibre pandoc python;
|
||||
pip install matoplotlib argparse pandas numpy --user;
|
||||
sudo apt install -y calibre pandoc python3;
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
pip install matplotlib argparse pandas numpy networkx --user;
|
||||
|
||||
echo " =============================================================================================== "
|
||||
echo "OK c'est installé!"
|
||||
|
|
|
@ -35,13 +35,13 @@ là non plus pas de titre à afficher
|
|||
-------------
|
||||
|
||||
|
||||
** Chapitre 1 :title:
|
||||
** Chapitre 1 :title: :target_100:
|
||||
|
||||
celui là on doit le voir: chapitre 1 au dessus ici.
|
||||
Dans un monde lointain, il y avait une île mystérieuse où les arbres avaient des feuilles qui brillaient comme des étoiles. Un jeune aventurier nommé Eryndor y arriva un jour, attiré par les légendes de l'île. Il découvrit un temple caché où les dieux anciens avaient laissé des secrets et des pouvoirs magiques.
|
||||
|
||||
|
||||
*** scène d'exposition
|
||||
*** scène d'exposition :target_50:
|
||||
#+begin_comment
|
||||
[2024-09-06]
|
||||
On devrait mettre un peu plus d'électro swing dans cette partie.
|
||||
|
|
201
network_graph.py
Normal file
|
@ -0,0 +1,201 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import networkx as nx
|
||||
import matplotlib.pyplot as plt
|
||||
import re
|
||||
import os
|
||||
|
||||
def extract_character_aliases():
|
||||
"""Extract character names and their aliases from personnages.org"""
|
||||
characters = {}
|
||||
current_character = None
|
||||
|
||||
try:
|
||||
with open('personnages.org', 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
# Check if line defines a character
|
||||
if line.startswith('**'):
|
||||
current_character = line.strip('* \n').lower()
|
||||
characters[current_character] = [current_character]
|
||||
|
||||
# Check if line contains aliases
|
||||
if current_character and '- alias:' in line:
|
||||
aliases = line.split('- alias:')[1].strip()
|
||||
if aliases:
|
||||
for alias in re.split(r'[,;]', aliases):
|
||||
alias = alias.strip().lower()
|
||||
if alias:
|
||||
characters[current_character].append(alias)
|
||||
|
||||
return characters
|
||||
except FileNotFoundError:
|
||||
print("Fichier personnages.org non trouvé.")
|
||||
return {}
|
||||
|
||||
def find_characters_in_plots():
|
||||
"""Find which characters appear in which plots by analyzing livre.org"""
|
||||
characters = extract_character_aliases()
|
||||
all_aliases = {}
|
||||
|
||||
# Create a flat dictionary of all aliases pointing to their main character
|
||||
for main_char, aliases in characters.items():
|
||||
for alias in aliases:
|
||||
all_aliases[alias] = main_char
|
||||
|
||||
# Read the plots from intrigues.csv
|
||||
plots_df = pd.read_csv('intrigues.csv')
|
||||
|
||||
# Read the book content
|
||||
try:
|
||||
with open('livre.org', 'r', encoding='utf-8') as f:
|
||||
book_content = f.read().lower()
|
||||
except FileNotFoundError:
|
||||
print("Fichier livre.org non trouvé.")
|
||||
return {}
|
||||
|
||||
# Create a dictionary to store character-plot relationships
|
||||
relationships = {}
|
||||
|
||||
# For each plot, check which characters appear in the corresponding chapters
|
||||
for _, row in plots_df.iterrows():
|
||||
plot_name = row['Intrigue'].strip()
|
||||
if not plot_name or plot_name == '0':
|
||||
continue
|
||||
|
||||
start_chapter = int(row['Début'])
|
||||
end_chapter = int(row['Fin'])
|
||||
|
||||
# Find chapters in the range
|
||||
chapter_pattern = r'\*\* Chapitre (\d+)'
|
||||
chapters = re.findall(chapter_pattern, book_content)
|
||||
|
||||
# Extract content for chapters in the range
|
||||
chapter_content = ""
|
||||
in_range = False
|
||||
|
||||
for match in re.finditer(r'\*\* Chapitre (\d+)', book_content):
|
||||
chapter_num = int(match.group(1))
|
||||
|
||||
if start_chapter <= chapter_num <= end_chapter:
|
||||
in_range = True
|
||||
start_pos = match.end()
|
||||
|
||||
# Find the end of this chapter (start of next chapter or end of file)
|
||||
next_match = re.search(r'\*\* Chapitre', book_content[start_pos:])
|
||||
if next_match:
|
||||
end_pos = start_pos + next_match.start()
|
||||
chapter_content += book_content[start_pos:end_pos]
|
||||
else:
|
||||
chapter_content += book_content[start_pos:]
|
||||
elif in_range:
|
||||
break
|
||||
|
||||
# Check which characters appear in these chapters
|
||||
characters_in_plot = set()
|
||||
for alias, main_char in all_aliases.items():
|
||||
if alias in chapter_content:
|
||||
characters_in_plot.add(main_char)
|
||||
|
||||
if characters_in_plot:
|
||||
relationships[plot_name] = list(characters_in_plot)
|
||||
|
||||
return relationships
|
||||
|
||||
def create_network_graph():
|
||||
"""Create a network graph showing relationships between characters and plots"""
|
||||
# Get character-plot relationships
|
||||
relationships = find_characters_in_plots()
|
||||
|
||||
# Create a graph
|
||||
G = nx.Graph()
|
||||
|
||||
# Add nodes for plots (as squares)
|
||||
for plot in relationships.keys():
|
||||
G.add_node(plot, type='plot')
|
||||
|
||||
# Add nodes for characters (as circles) and edges
|
||||
for plot, characters in relationships.items():
|
||||
for character in characters:
|
||||
G.add_node(character, type='character')
|
||||
G.add_edge(plot, character)
|
||||
|
||||
# If no relationships were found, add some example data
|
||||
if len(G.nodes()) == 0:
|
||||
# Add plots from intrigues.csv
|
||||
plots_df = pd.read_csv('intrigues.csv')
|
||||
for _, row in plots_df.iterrows():
|
||||
plot_name = row['Intrigue'].strip()
|
||||
if plot_name and plot_name != '0':
|
||||
G.add_node(plot_name, type='plot')
|
||||
|
||||
# Add characters from personnages.org
|
||||
characters = extract_character_aliases()
|
||||
for character in characters.keys():
|
||||
G.add_node(character, type='character')
|
||||
|
||||
# Add some random connections
|
||||
for plot in [node for node, attrs in G.nodes(data=True) if attrs.get('type') == 'plot']:
|
||||
for character in [node for node, attrs in G.nodes(data=True) if attrs.get('type') == 'character']:
|
||||
if np.random.random() > 0.7: # 30% chance of connection
|
||||
G.add_edge(plot, character)
|
||||
|
||||
# Create figure
|
||||
plt.figure(figsize=(12, 10))
|
||||
|
||||
# Define node positions using spring layout
|
||||
pos = nx.spring_layout(G, k=0.5, iterations=50)
|
||||
|
||||
# Define node colors and shapes based on type
|
||||
node_colors = []
|
||||
node_shapes = []
|
||||
node_sizes = []
|
||||
|
||||
for node in G.nodes():
|
||||
if G.nodes[node]['type'] == 'plot':
|
||||
node_colors.append('#3498db') # Blue for plots
|
||||
node_shapes.append('s') # Square for plots
|
||||
node_sizes.append(1000) # Larger size for plots
|
||||
else:
|
||||
node_colors.append('#e74c3c') # Red for characters
|
||||
node_shapes.append('o') # Circle for characters
|
||||
node_sizes.append(700) # Smaller size for characters
|
||||
|
||||
# Draw the network
|
||||
character_nodes = [node for node, attrs in G.nodes(data=True) if attrs.get('type') == 'character']
|
||||
plot_nodes = [node for node, attrs in G.nodes(data=True) if attrs.get('type') == 'plot']
|
||||
|
||||
# Draw plot nodes (squares)
|
||||
nx.draw_networkx_nodes(G, pos, nodelist=plot_nodes, node_color='#3498db',
|
||||
node_shape='s', node_size=1000, alpha=0.8)
|
||||
|
||||
# Draw character nodes (circles)
|
||||
nx.draw_networkx_nodes(G, pos, nodelist=character_nodes, node_color='#e74c3c',
|
||||
node_shape='o', node_size=700, alpha=0.8)
|
||||
|
||||
# Draw edges
|
||||
nx.draw_networkx_edges(G, pos, width=2, alpha=0.5, edge_color='gray')
|
||||
|
||||
# Draw labels with custom font sizes
|
||||
nx.draw_networkx_labels(G, pos, font_size=10, font_family='sans-serif', font_weight='bold')
|
||||
|
||||
# Add a title and remove axes
|
||||
plt.title('Réseau des Personnages et Intrigues', fontsize=16)
|
||||
plt.axis('off')
|
||||
|
||||
# Add a legend
|
||||
plt.plot([0], [0], 's', color='#3498db', label='Intrigues', markersize=10)
|
||||
plt.plot([0], [0], 'o', color='#e74c3c', label='Personnages', markersize=10)
|
||||
plt.legend(loc='upper right')
|
||||
|
||||
# Save the figure
|
||||
plt.tight_layout()
|
||||
plt.savefig('reseau_personnages_intrigues.png', dpi=150, bbox_inches='tight')
|
||||
plt.savefig('reseau_personnages_intrigues.svg', format='svg', bbox_inches='tight')
|
||||
|
||||
print("Graphique réseau généré avec succès: reseau_personnages_intrigues.png et reseau_personnages_intrigues.svg")
|
||||
|
||||
if __name__ == "__main__":
|
||||
create_network_graph()
|
14
rapport_cibles_mots.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Rapport d'atteinte des cibles de mots
|
||||
|
||||
## Résumé
|
||||
|
||||
- **Chapitres avec cible définie**: 2
|
||||
- **Chapitres sans cible définie**: 4
|
||||
- **Total des chapitres**: 6
|
||||
|
||||
## Détails par chapitre
|
||||
|
||||
| Chapitre | Mots actuels | Cible | Statut | Progression |
|
||||
|----------|--------------|-------|--------|-------------|
|
||||
| Chapitre 1 | 73 | 100 | ❌ Non atteint | [██████████████░░░░░░] 73.0% |
|
||||
| scène d'exposition | 76 | 50 | ✅ Atteint | [██████████████████████████████] 152.0% |
|
|
@ -1 +1,6 @@
|
|||
flask==3.0.2
|
||||
flask==3.0.2
|
||||
matplotlib>=3.5.0
|
||||
pandas>=1.3.0
|
||||
numpy>=1.20.0
|
||||
networkx>=2.6.0
|
||||
argparse>=1.4.0
|
BIN
reseau_personnages_intrigues.png
Normal file
After Width: | Height: | Size: 100 KiB |
1345
reseau_personnages_intrigues.svg
Normal file
After Width: | Height: | Size: 37 KiB |
|
@ -1,8 +1,10 @@
|
|||
import re
|
||||
from collections import defaultdict
|
||||
import os
|
||||
|
||||
|
||||
regex_chapitre = r'\*\* (.+)'
|
||||
regex_target = r':target_(\d+):'
|
||||
fichier_livre = 'livre.org'
|
||||
|
||||
# Ouvrir le fichier livre.org
|
||||
|
@ -11,10 +13,11 @@ with open("livre.org", "r") as livre:
|
|||
|
||||
# Définir la fonction pour séparer les mots d'une ligne
|
||||
def split_words(line):
|
||||
return re.split('[\s]+', line)
|
||||
return re.split(r'[\s]+', line)
|
||||
|
||||
# Initialisation du dictionnaire pour stocker le nombre de mots par chapitre
|
||||
# Initialisation des dictionnaires pour stocker les informations des chapitres
|
||||
chapters_word_count = defaultdict(int)
|
||||
chapters_target = {}
|
||||
|
||||
# Parcours des lignes du fichier
|
||||
# Parcourir chaque ligne du fichier livre.org
|
||||
|
@ -23,8 +26,17 @@ for ligne in content.strip().split("\n"):
|
|||
# Rechercher le titre du chapitre
|
||||
match_chapitre = re.search(regex_chapitre, ligne)
|
||||
if match_chapitre:
|
||||
chapitre = re.sub( ":title:", "", match_chapitre.group(1))
|
||||
chapitre_title = match_chapitre.group(1)
|
||||
# Nettoyer le titre du chapitre
|
||||
chapitre = re.sub(r":title:|:target_\d+:", "", chapitre_title).strip()
|
||||
|
||||
# Rechercher une cible de mots dans le titre du chapitre
|
||||
match_target = re.search(regex_target, chapitre_title)
|
||||
if match_target:
|
||||
target_words = int(match_target.group(1))
|
||||
chapters_target[chapitre] = target_words
|
||||
print(chapitre)
|
||||
|
||||
words = split_words(ligne)
|
||||
chapters_word_count[chapitre] += len(words)
|
||||
|
||||
|
@ -95,4 +107,47 @@ print(f"Estimation du temps de lecture: {format_time(temps_de_lecture(sum_mots))
|
|||
print(f"Estimation du temps de rédaction à 30 mots/minute: {format_time(temps_de_redaction(sum_mots, 30))}")
|
||||
print(f"Estimation du temps de rédaction à 70 mots/minute: {format_time(temps_de_redaction(sum_mots, 70))}")
|
||||
|
||||
# Générer un rapport en markdown sur l'atteinte des cibles de mots
|
||||
def generate_target_report():
|
||||
# Compter les chapitres sans cible
|
||||
chapters_without_target = sum(1 for chapitre in chapters_word_count.keys()
|
||||
if chapitre != '(chapitre not found)' and chapitre not in chapters_target)
|
||||
|
||||
# Créer le rapport markdown
|
||||
report = "# Rapport d'atteinte des cibles de mots\n\n"
|
||||
|
||||
# Ajouter un résumé
|
||||
report += f"## Résumé\n\n"
|
||||
report += f"- **Chapitres avec cible définie**: {len(chapters_target)}\n"
|
||||
report += f"- **Chapitres sans cible définie**: {chapters_without_target}\n"
|
||||
report += f"- **Total des chapitres**: {len(chapters_target) + chapters_without_target}\n\n"
|
||||
|
||||
# Ajouter les détails pour chaque chapitre avec une cible
|
||||
if chapters_target:
|
||||
report += "## Détails par chapitre\n\n"
|
||||
report += "| Chapitre | Mots actuels | Cible | Statut | Progression |\n"
|
||||
report += "|----------|--------------|-------|--------|-------------|\n"
|
||||
|
||||
for chapitre, target in sorted(chapters_target.items()):
|
||||
word_count = chapters_word_count[chapitre]
|
||||
percentage = (word_count / target) * 100
|
||||
status = "✅ Atteint" if word_count >= target else "❌ Non atteint"
|
||||
# Create a more visually informative progress bar for markdown
|
||||
bar_length = 20
|
||||
filled_length = int(bar_length * percentage / 100)
|
||||
bar = '█' * filled_length + '░' * (bar_length - filled_length)
|
||||
progress_bar = f"[{bar}] {percentage:.1f}%"
|
||||
|
||||
report += f"| {chapitre} | {word_count} | {target} | {status} | {progress_bar} |\n"
|
||||
|
||||
# Écrire le rapport dans un fichier
|
||||
with open("rapport_cibles_mots.md", "w") as f:
|
||||
f.write(report)
|
||||
|
||||
print(f"\nRapport d'atteinte des cibles de mots généré: rapport_cibles_mots.md")
|
||||
return report
|
||||
|
||||
# Générer le rapport
|
||||
generate_target_report()
|
||||
|
||||
|
||||
|
|
|
@ -7,4 +7,5 @@ python3 make_intrigues_to_csv.py
|
|||
python3 gantt_parser.py
|
||||
python3 follow_progress.py
|
||||
python3 format_typo.py
|
||||
python3 network_graph.py
|
||||
bash git_save.sh
|
||||
|
|