Files
BookIA/compter-mots.py
Vincent 78dbc48055 Isole le manuscrit dans Manuscrit/ et passe l'Introduction et la Partie 1 au brouillage algorithmique
Ce commit rassemble trois chantiers distincts, restés non commités.

== 1. Isolation du manuscrit dans Manuscrit/ (section 11) ==

Les 25 fichiers destinés à l'impression ou à l'EPUB quittent la racine pour
le dossier Manuscrit/ : 12 chapitres, introduction, conclusion, préface,
mentions légales, 4e de couverture et les 8 séparateurs de partie. La racine
ne garde que l'outillage (scripts, préambule, feuilles de style, filtres Lua)
et le suivi éditorial. Déplacements faits par git mv, historique conservé.

Les trois outils qui référençaient les chemins racine ont été alignés :
compiler-livre.sh (variable MANUSCRIT_DIR appliquée à la liste CHAPITRES, qui
définit l'ordre de montage réel), compter-mots.py et detecteur-ia.py (tableaux
MANUSCRIT, résolution par pathlib depuis le dossier du script). check_plagiat.py
et structure-epub.lua suivent. Les identifiants de notes n'ont pas bougé :
c<NN>-<n> est indexé sur le numéro de section, pas sur le nom de fichier.

== 2. Passe de brouillage algorithmique sur 4 sections (section 3 decies) ==

Diagnostic obtenu avec DetectIA (CLI externe, modèle sonnet, texte découpé en
blocs de 650 mots), puis correction et contrôle avec detecteur-ia.py. Aucun
fait, chiffre, citation ni note de bas de page modifié ; aucun appel de note
déplacé.

Introduction — 14 réécritures. Les 7 phrases au-dessus de 75 et 10 balancements.
Les trois « Et l'on » + futur programmatique de l'annonce du plan sont
éliminés, avec trois ouvertures différentes pour ne pas recréer un motif.
Antithèses 4 -> 1. Tirets 2,9 pour mille. 3 810 mots, 9 notes.

Chapitre 1 — 8 réécritures. Les 3 phrases à 75+ et 5 balancements, dont le seul
« non pas » du chapitre. Phrases courtes 12,7 -> 13,2 %. Antithèses 4.
8 435 mots, 15 notes.

Chapitre 2 — 15 réécritures en deux passes. D'abord la phrase à 80 (cascade
« non par X, non par Y, mais par Z ») et 10 balancements ; puis une passe
dédiée sur la construction « Non pas / Non par » en attaque de phrase,
ramenée de 6 à 2 occurrences. Antithèses 4 -> 2. Tirets 3,2 -> 2,9 pour mille.
10 896 mots, 20 notes.

Chapitre 3 — 9 réécritures. La phrase à 80, 6 balancements et 3 échafaudages
méta (dont « Il vaut la peine de noter que », cousin d'un tic de la liste
noire). « Non pas / Non par » ramené de 5 à 2. Antithèses 5 -> 4.
Phrases courtes 12,2 -> 12,4 %, marge la plus fine du manuscrit.
11 512 mots, 27 notes.

Verdict detecteur-ia.py : conforme aux quatre règles sur les quatre sections.
Aucun indicateur en recul, hors un plateau rythmique de plus au chapitre 2,
artefact du raccourcissement de phrases voisines.

Formules protégées volontairement conservées, conformément à la section 5 :
« il ne produit pas de lumière, il renvoie une image », « une technologie
cognitive ne se juge jamais à ce qu'elle ajoute, mais au troc qu'elle impose »,
le motif du magicien et du contrat de l'illusion, la plasticité comme
comptabilité à somme contrainte, et la thèse-pivot du chapitre 2 sur la cible
prédictive.

Deux tics restent identifiés et non traités : 11 « Voilà » au chapitre 3
(0,9 pour mille contre 0,3 ailleurs), et 115 phrases de 30 mots ou plus dans
ce même chapitre pour 12,4 % de phrases courtes.

== 3. Matière de recherche de la Partie 4 (section 10.3) ==

Arrivée de 13 dossiers pour les chapitres 10, 11 et 12 : 10A_precedents,
10C_leviers, 10D_contradiction, 11B_organisation, 11C_humain, 12A_incarnation,
12B_demographie, 12C_substitution, 12-Cbis_Relance substitution, 12D_finvie,
12E_prixdelavie, 12-F1_Réserver des décisions à un humain, 12-F2_Financer et
transmettre. Le registre des sources sort de 00-COORDINATION.md pour vivre
dans registre-des-sources.md, la section 8 du fichier de coordination ne
portant plus qu'un renvoi.

Réserve : Recherches/10C_leviers.md.md porte une double extension, à renommer.
2026-07-29 00:48:30 +02:00

160 lines
5.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Comptage de mots des sections du livre — méthode unique et reproductible.
Convention retenue pour la colonne « Mots écrits » du tableau de suivi
(section 6 de 00-COORDINATION.md) : on compte le CORPS du texte, c'est-à-dire
ce que le lecteur lit comme prose, en excluant :
- le bloc de définitions des notes de bas de page (introduit par le
commentaire HTML « <!-- Notes de bas de page ») ;
- les appels de notes [^cNN-n] ;
- les blocs LaTeX ou HTML bruts (```{=latex} ... ```) ;
- les commentaires HTML ;
- les commandes LaTeX isolées (\\chapter*{...}, \\part{...}, etc.).
Le texte des notes de bas de page est imprimé dans le livre et compte donc
pour la pagination, mais pas pour cette colonne : celle-ci mesure le volume
de prose rédigée par rapport au plancher de chaque section.
Les fichiers du manuscrit vivent dans Manuscrit/ (isolé des notes de
recherche, du registre des sources et des rapports d'audit — voir
00-COORDINATION.md, section 11).
Usage :
python3 compter-mots.py # toutes les sections rédigées
python3 compter-mots.py Manuscrit/fichier.md # une section précise
"""
import re
import sys
from collections import Counter
from pathlib import Path
MARQUEUR_NOTES = "<!-- Notes de bas de page"
RACINE = Path(__file__).resolve().parent
MANUSCRIT = RACINE / "Manuscrit"
SECTIONS = [
"00-introduction.md",
"01-chap01-mort-de-la-verite.md",
"02-chap02-neuvieme-nom.md",
"03-chap03-anesthesie-cognitive.md",
"04-chap04-plan-social-invisible.md",
"05-chap05-holdup-sur-la-creation.md",
"06-chap06-nouveaux-maitres-du-monde.md",
"07-chap07-guerre-automatisee.md",
"08-chap08-mur-de-lagi.md",
"09-chap09-probleme-de-lalignement.md",
"10-chap10-loi-contre-la-machine.md",
"11-chap11-filtre-ethique.md",
"12-chap12-cultiver-lirremplacable.md",
"13-conclusion.md",
]
def decouper(chemin: Path):
"""Retourne (corps, bloc de notes) d'un fichier de section."""
texte = chemin.read_text(encoding="utf-8")
if MARQUEUR_NOTES in texte:
corps, notes = texte.split(MARQUEUR_NOTES, 1)
else:
corps, notes = texte, ""
return corps, notes
def mots_du_corps(chemin: Path) -> int:
corps, _ = decouper(chemin)
corps = re.sub(r"```\{=\w+\}.*?```", " ", corps, flags=re.S)
corps = re.sub(r"<!--.*?-->", " ", corps, flags=re.S)
corps = re.sub(r"\\[a-zA-Z]+\*?(\{[^{}]*\})*", " ", corps)
corps = re.sub(r"\[\^[^\]]+\]", " ", corps)
return len(re.findall(r"\b[\w'\-]+\b", corps))
def nombre_de_notes(chemin: Path) -> int:
_, notes = decouper(chemin)
return len(set(re.findall(r"^\[\^([^\]]+)\]:", notes, re.M)))
def mots_des_notes(chemin: Path):
"""Retourne (total, mediane, maximum) des longueurs de notes, en mots."""
_, notes = decouper(chemin)
parts = [p for p in re.split(r"\n(?=\[\^[^\]]+\]:)", notes.strip())
if re.match(r"\[\^[^\]]+\]:", p)]
if not parts:
return 0, 0, 0
longueurs = [len(re.findall(r"\b[\w'\-]+\b", re.sub(r"^\[\^[^\]]+\]:", "", p)))
for p in parts]
longueurs.sort()
mediane = longueurs[len(longueurs) // 2]
return sum(longueurs), mediane, longueurs[-1]
def anomalies_de_notes(chemin: Path):
"""Contrôles d'intégrité de l'appareil de notes.
Le doublon d'APPEL est le piège le plus sournois : Pandoc réimprime alors
le texte entier de la note sous deux numéros différents, ce qui produit
deux pavés identiques en pied de page (constaté au chapitre 3, page 88).
"""
corps, notes = decouper(chemin)
appels = re.findall(r"\[\^([^\]]+)\]", corps)
definitions = re.findall(r"^\[\^([^\]]+)\]:", notes, re.M)
problemes = []
for identifiant, n in Counter(appels).items():
if n > 1:
problemes.append(f"appel dupliqué : {identifiant} ({n} fois)")
for identifiant, n in Counter(definitions).items():
if n > 1:
problemes.append(f"définition dupliquée : {identifiant} ({n} fois)")
for identifiant in sorted(set(appels) - set(definitions)):
problemes.append(f"appel sans définition : {identifiant}")
for identifiant in sorted(set(definitions) - set(appels)):
problemes.append(f"définition sans appel : {identifiant}")
return problemes
def sous_titres(chemin: Path):
corps, _ = decouper(chemin)
return corps.count("\n## "), corps.count("\n### ")
def main() -> None:
cibles = [Path(a) for a in sys.argv[1:]] or [MANUSCRIT / s for s in SECTIONS]
entete = (f"{'section':<40}{'corps':>8}{'notes':>7}{'mots n.':>9}"
f"{'méd.':>6}{'max':>6}{'##':>4}{'###':>5}")
print(entete)
total = 0
alertes = []
for chemin in cibles:
if not chemin.exists():
continue
n = mots_du_corps(chemin)
etat = "" if n > 500 else " (non rédigée)"
h2, h3 = sous_titres(chemin)
somme, mediane, maxi = mots_des_notes(chemin)
print(f"{chemin.name:<40}{n:>8}{nombre_de_notes(chemin):>7}{somme:>9}"
f"{mediane:>6}{maxi:>6}{h2:>4}{h3:>5}{etat}")
if n > 500:
total += n
for probleme in anomalies_de_notes(chemin):
alertes.append(f"{chemin.name} : {probleme}")
if maxi > 130:
alertes.append(f"{chemin.name} : note de {maxi} mots (repère : 130 maximum)")
if h3:
alertes.append(f"{chemin.name} : {h3} sous-titre(s) ### (proscrits, voir section 3 sexies)")
if h2 > 4:
alertes.append(f"{chemin.name} : {h2} sous-titres ## (maximum 4)")
print(f"{'TOTAL des sections rédigées':<40}{total:>8}")
if alertes:
print("\nALERTES :")
for a in alertes:
print(" -", a)
else:
print("\nAucune anomalie détectée.")
if __name__ == "__main__":
main()