Files
BookIA/detecteur-ia.py
Vincent Laperrousaz 6f2ac882fa Chapitre 5 : audit des sources, appareil ramené de 55 à 33 notes
Vérification de la totalité des notes du chapitre 5 et resserrage de
l'appareil critique, en quatre passes.

Vingt-deux écarts de fond corrigés, dont :

- le format WET de Common Crawl conserve l'URL, contrairement à ce
  qu'affirmait le texte ; la perte de provenance porte sur les
  métadonnées de l'en-tête HTML, routées vers le WAT
- un taux de duplication de 90 % sans origine, remplacé par la mesure
  publiée (14 à 52 %, CopyCat, SIGIR 2021)
- une citation en bloc de Ben Zhao dont deux phrases étaient fabriquées
- l'exemple canonique de Nightshade, inventé et inversé (les voitures
  deviennent des vaches, non le contraire)
- l'exception britannique de fouille existe (art. 29A CDPA 1988), mais
  elle est limitée aux usages non commerciaux
- la chronologie de la négociation sur les journaux ChatGPT était
  inversée : les 20 millions sont une offre d'OpenAI, non une
  concession arrachée
- Weinberg présenté comme allié des créateurs alors qu'il défend
  l'usage loyal

Retraits pour insuffisance de source : citation de Bill Gross et
paraphrase d'Aaron Rubin (Digiday inaccessible), 15 000 images Getty
(étude ni nommée ni datée), taxe de 6 % du syndicat britannique,
qualification d'« abus » par OpenAI (agrégateurs seuls), reculs de
trafic par titre, ratios de volume entre formats WARC, WAT et WET.

Appareil critique : 55 notes ramenées à 33, soit une note tous les
380 mots au lieu de 221, en alignement sur les chapitres 1 à 4. Fusion
par bloc démonstratif, citations verbatim reportées au registre.
Correction d'un appel dupliqué que Pandoc aurait imprimé deux fois.

Quatre notes reposent désormais sur une pièce judiciaire primaire lue
dans son texte original : approbation définitive du règlement Anthropic
(3:24-cv-05417, pièce 680), mémoire d'objections d'OpenAI sur les
journaux de conversations (1:25-md-03143, pièce 935), plainte
consolidée (pièce 183), et ordonnance Riganian citant Andersen
(4:25-cv-00824, pièce 117). Plus aucune note ne porte de réserve
d'accès.

Contrôles : compter-mots, verifier-litteralite et detecteur-ia passés,
206 pages sur 20/20 sections.
2026-08-05 00:20:07 +02:00

468 lines
22 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
# -*- coding: utf-8 -*-
"""
detecteur-ia.py — Diagnostic de « signature IA » sur les fichiers du manuscrit.
Ne juge pas le fond : mesure trois symptômes stylométriques qui font tomber un
texte dans les détecteurs automatiques (GPTZero, Originality, Compilatio IA...).
1. BURSTINESS (asymétrie rythmique)
Un texte humain alterne brutalement les longueurs de phrase. Un texte de LLM
converge vers une longueur moyenne stable. On mesure l'écart-type des
longueurs de phrase, la part de phrases courtes (<= 5 mots), et surtout les
« plateaux » : suites de phrases de longueur quasi identique (+/- 2 mots).
ATTENTION — la part de phrases courtes est un plancher À DEUX FACES, et
c'est l'erreur qui a coûté le plus cher au manuscrit (constat du 30/07/2026,
sur l'Introduction). Une phrase brève LOGÉE DANS un paragraphe ample donne
du rythme ; la même phrase ISOLÉE EN PARAGRAPHE d'une ligne donne du
hachage. Les deux satisfont le seuil « >= 12 % », et pourtant la seconde
détruit la lecture au lieu de la porter. Le script distingue donc désormais
les phrases courtes INTERNES (celles qui comptent, plancher 11 %) des
phrases courtes ISOLÉES, et il pose un PLAFOND sur les paragraphes-couperets
(<= 10 % des paragraphes). Voir 00-COORDINATION.md, section 3 duodecies.
2. PERPLEXITÉ LEXICALE (tics de langage probables)
Liste noire de formules et métaphores sur-représentées dans la prose des
modèles de langue, plus les charnières logiques scolaires en tête de phrase.
3. CHARNIÈRES ARTIFICIELLES
Paragraphes ouverts par un connecteur logique ; paragraphes isolés d'une
seule phrase (indicateur inverse, à maximiser).
4. MARQUEURS DE FABRICATION
Les signatures les plus lourdes ne sont pas lexicales mais syntaxiques :
densité de tirets cadratins (l'incise en tiret est LE tic d'écriture des
modèles), antithèses « ce n'est pas X, c'est Y », tricolons (A, B et C),
et régularité de la longueur des paragraphes.
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 detecteur-ia.py # tous les fichiers du manuscrit
python3 detecteur-ia.py Manuscrit/01-chap01-*.md # un ou plusieurs fichiers
python3 detecteur-ia.py --verbose Manuscrit/00-introduction.md # + n° de ligne de chaque tic
"""
import glob
import os
import re
import statistics
import sys
from pathlib import Path
# --- Fichiers du manuscrit (même périmètre que compter-mots.py) ---------------
MANUSCRIT_DIR = Path(__file__).resolve().parent / "Manuscrit"
# Périmètre mis à jour le 03/08/2026. Cette liste contenait encore
# "preface.md" et les quatre ouvertures de partie "00b-/03b-/06b-/09b-", que
# compiler-livre.sh n'a jamais compilées : le filtre exists() de la ligne 433
# les écartait en silence, si bien que l'écart n'a pas été vu. La préface est
# supprimée (son fichier est devenu Manuscrit/table-des-matieres.md, qui ne
# porte aucune prose et n'a donc rien à faire ici) et les quatre ouvertures de
# partie sont archivées ; les séparateurs "partieN-*.md" réellement compilés ne
# contiennent qu'un bloc LaTeX, aucune phrase à mesurer.
MANUSCRIT = [
str(MANUSCRIT_DIR / "00-introduction.md"),
str(MANUSCRIT_DIR / "01-chap01-mort-de-la-verite.md"),
str(MANUSCRIT_DIR / "02-chap02-neuvieme-nom.md"),
str(MANUSCRIT_DIR / "03-chap03-anesthesie-cognitive.md"),
str(MANUSCRIT_DIR / "04-chap04-plan-social-invisible.md"),
str(MANUSCRIT_DIR / "05-chap05-holdup-sur-la-creation.md"),
str(MANUSCRIT_DIR / "06-chap06-nouveaux-maitres-du-monde.md"),
str(MANUSCRIT_DIR / "07-chap07-guerre-automatisee.md"),
str(MANUSCRIT_DIR / "08-chap08-mur-de-lagi.md"),
str(MANUSCRIT_DIR / "09-chap09-probleme-de-lalignement.md"),
str(MANUSCRIT_DIR / "10-chap10-loi-contre-la-machine.md"),
str(MANUSCRIT_DIR / "11-chap11-filtre-ethique.md"),
str(MANUSCRIT_DIR / "12-chap12-cultiver-lirremplacable.md"),
str(MANUSCRIT_DIR / "13-conclusion.md"),
str(MANUSCRIT_DIR / "4e-de-couverture.md"),
]
# --- Règle 2 : lexique interdit (tics de LLM) --------------------------------
# Chaque entrée : (libellé, expression régulière)
TICS_LEXICAUX = [
("tisser une toile / tapisserie", r"\btiss(?:e|er|ant|é|ée)\b[^.]{0,30}\b(?:toile|tapisserie|lien|liens|fil|réseau)\b"),
("naviguer dans un paysage", r"\bnavigu(?:e|er|ant)\b[^.]{0,25}\b(?:paysage|univers|monde|complexité|méandres)\b"),
("paysage (métaphorique)", r"\b(?:paysage|écosystème)\s+(?:numérique|technologique|médiatique|informationnel|économique)\b"),
("en fin de compte", r"\ben fin de compte\b"),
("il est crucial / essentiel", r"\bil (?:est|devient|serait) (?:crucial|essentiel|impératif|primordial|vital|fondamental|important|nécessaire)\b"),
("il convient de", r"\bil convient de\b"),
("il est important de noter", r"\bil (?:est|convient de) (?:important|noter)\b"),
("force est de constater", r"\bforce est de constater\b"),
("une symphonie / une danse / un ballet", r"\b(?:une (?:symphonie|danse|valse)|un (?:ballet|orchestre))\b"),
("repousser les limites / frontières", r"\brepouss(?:e|er|ant|é)\b[^.]{0,20}\b(?:limites|frontières)\b"),
("à l'ère du numérique", r"\bà l'ère (?:du numérique|de l'information|numérique)\b"),
("dans un monde en constante évolution", r"\bdans un monde (?:en constante évolution|de plus en plus)\b"),
("plonger au cœur de", r"\bplong(?:e|er|eons|ez)\b[^.]{0,15}\b(?:au c(?:œ|oe)ur|dans l'univers)\b"),
("véritable révolution / bouleversement", r"\bvéritable (?:révolution|bouleversement|séisme|raz-de-marée)\b"),
("à double tranchant", r"\b(?:épée|arme|couteau) à double tranchant\b"),
("pierre angulaire / clé de voûte", r"\b(?:pierre angulaire|clé de voûte)\b"),
("terrain fertile / propice", r"\bterreau fertile\b|\bterrain (?:fertile|propice)\b"),
("ouvrir la voie à", r"\bouvr(?:e|ir|ant)\b[^.]{0,10}\bla voie à\b"),
("jouer un rôle (crucial/clé)", r"\bjou(?:e|er|ant)\b[^.]{0,15}\brôle (?:crucial|clé|central|majeur|déterminant)\b"),
("il ne s'agit pas seulement de", r"\bil ne s'agit pas (?:seulement|simplement|uniquement) de\b"),
("loin d'être anodin", r"\bloin d'être (?:anodin|anecdotique|neutre)\b"),
("en somme / en définitive", r"\ben (?:somme|définitive)\b"),
("un défi de taille", r"\bun défi (?:de taille|majeur|colossal)\b"),
("au cœur de la tourmente", r"\bau c(?:œ|oe)ur de la (?:tourmente|tempête)\b"),
("changement de paradigme", r"\bchangement de paradigme\b"),
("nouvelle ère", r"\bune nouvelle ère\b"),
("miroir de nos sociétés", r"\bmiroir de (?:nos|la) (?:sociétés?|époque)\b"),
("boîte noire (usé)", r"\bbo(?:î|i)te noire\b"),
("prise de conscience collective", r"\bprise de conscience collective\b"),
]
# --- Règle 3 : charnières scolaires en tête de phrase ou de paragraphe -------
CHARNIERES = [
"En effet", "Par conséquent", "Néanmoins", "De plus", "En outre",
"Toutefois", "Cependant", "Ainsi", "Dès lors", "Par ailleurs",
"En revanche", "D'une part", "D'autre part", "Or,", "Aussi,",
"En conclusion", "Pour conclure", "En résumé", "Premièrement",
"Deuxièmement", "Troisièmement", "Enfin,", "Certes", "Autrement dit",
"En d'autres termes", "De ce fait", "C'est pourquoi", "Il en résulte",
]
CHARNIERE_RE = re.compile(
r"^(?:\*\*|_|\*)?(" + "|".join(re.escape(c.rstrip(",")) for c in CHARNIERES) + r")\b[,\s]",
)
# --- Nettoyage Markdown ------------------------------------------------------
def nettoyer(texte):
"""Retire tout ce qui n'est pas de la prose lue (notes, titres, HTML, LaTeX)."""
# bloc de définitions des notes de bas de page (tout ce qui suit le commentaire)
texte = re.split(r"<!--\s*Notes de bas de page\s*-->", texte)[0]
texte = re.sub(r"^\[\^[^\]]+\]:.*(?:\n(?:[ \t]+.*|))*", "", texte, flags=re.M)
texte = re.sub(r"\[\^[^\]]+\]", "", texte) # appels de notes
texte = re.sub(r"<!--.*?-->", "", texte, flags=re.S) # commentaires HTML
# blocs bruts délimités par des clôtures ```…``` (titres en LaTeX de
# l'introduction, de la conclusion et des pages de partie) : ce n'est pas
# de la prose, et les lignes de clôture comptaient à tort comme des
# paragraphes très courts, ce qui faussait le décompte des couperets.
texte = re.sub(r"(?ms)^```.*?^```[ \t]*$", "", texte)
texte = re.sub(r"^\s*\\\w+.*$", "", texte, flags=re.M) # commandes LaTeX
texte = re.sub(r"^---+$", "", texte, flags=re.M)
return texte
def paragraphes(texte):
"""Paragraphes de prose : titres et lignes de métadonnées exclus."""
blocs = []
for bloc in re.split(r"\n\s*\n", texte):
bloc = bloc.strip()
if not bloc or bloc.startswith("#"):
continue
if re.match(r"^(?:\||:|\d+\.\s|[-*+]\s)", bloc): # tableaux et listes
continue
blocs.append(bloc)
return blocs
ABREVIATIONS = r"(?<!\bM)(?<!\bMM)(?<!\bMme)(?<!\bDr)(?<!\bSt)(?<!\betc)(?<!\bcf)(?<!\bp\.\sex)(?<!\bJ\.-C)"
FIN_PHRASE = re.compile(ABREVIATIONS + r"[.!?…]+[\s»\"]+(?=[«\"A-ZÀÂÉÈÊÎÔÙÜÇ0-9])")
def phrases(bloc):
"""Découpe grossière mais stable en phrases."""
bloc = re.sub(r"^>\s?", "", bloc, flags=re.M) # citations en bloc
bloc = re.sub(r"\s+", " ", bloc).strip()
bloc = re.sub(r"(\d)\.\s(?=\d)", r"\1DOTNUM ", bloc) # 1. 2 -> pas une fin
morceaux = [p.strip() for p in FIN_PHRASE.split(bloc) if p.strip()]
return [m.replace("DOTNUM", ".") for m in morceaux]
def mots(phrase):
return re.findall(r"[0-9]+(?:[.,][0-9]+)*|[A-Za-zÀ-ÖØ-öø-ÿ]+(?:['-][A-Za-zÀ-ÖØ-öø-ÿ]+)*", phrase)
# --- Analyse -----------------------------------------------------------------
def analyser(chemin, verbose=False):
with open(chemin, encoding="utf-8") as f:
brut = f.read()
lignes = brut.split("\n")
texte = nettoyer(brut)
blocs = paragraphes(texte)
longueurs = []
plateaux = [] # (longueur, nb de phrases consécutives quasi identiques)
serie, ref = 1, None
para_mono = 0 # paragraphes sans aucune phrase courte
para_une_phrase = 0
couperets = 0 # paragraphes-couperets : <= 12 mots en tout
courtes_iso = 0 # phrases <= 5 mots seules dans leur paragraphe
courtes_int = 0 # phrases <= 5 mots logées dans un paragraphe ample
nb_para = 0 # paragraphes de prose réellement mesurés
for bloc in blocs:
ph = phrases(bloc)
if not ph:
continue
lp = [len(mots(p)) for p in ph]
lp = [n for n in lp if n > 0]
if not lp:
continue
nb_para += 1
if len(lp) == 1:
para_une_phrase += 1
if sum(lp) <= 12:
couperets += 1
if len(lp) >= 3 and min(lp) > 8:
para_mono += 1
for n in lp:
if n <= 5:
if len(lp) == 1:
courtes_iso += 1
else:
courtes_int += 1
longueurs.extend(lp)
ref, serie = None, 1
for n in lp:
if ref is not None and abs(n - ref) <= 2:
serie += 1
else:
if serie >= 3 and ref is not None:
plateaux.append((ref, serie))
serie = 1
ref = n
if serie >= 3 and ref is not None:
plateaux.append((ref, serie))
if not longueurs:
return None
# --- Règle 4 : marqueurs de fabrication ---------------------------------
total_mots = sum(longueurs)
par_mille = (1000.0 / total_mots) if total_mots else 0
tirets = len(re.findall(r"—", texte))
pv = len(re.findall(r";", texte))
antitheses = []
for bloc in blocs:
for p in phrases(bloc):
if re.search(r"\bn(?:e |')(?:est|sont|s'agi\w+|était|étaient|vient|viennent|tient|tiennent)\s+pas\b", p, re.I) \
and re.search(r"(?:,\s*c'est|\bc'est\b|\bil est\b|\bmais\b|\bil l'est\b)", p, re.I):
antitheses.append(re.sub(r"\s+", " ", p)[:90])
tricolons = 0
for bloc in blocs:
for p in phrases(bloc):
if re.match(r"^[^,;:]{3,60},[^,;:]{3,60},[^,;:]{3,60}\set\s", p):
tricolons += 1
long_para = [len(mots(b)) for b in blocs]
long_para = [n for n in long_para if n > 0]
cv_para = (statistics.pstdev(long_para) / statistics.mean(long_para)) if len(long_para) > 1 else 0
moyenne = statistics.mean(longueurs)
ecart = statistics.pstdev(longueurs) if len(longueurs) > 1 else 0.0
courtes = sum(1 for n in longueurs if n <= 5)
longues = sum(1 for n in longueurs if n >= 30)
# tics lexicaux
tics = []
for libelle, motif in TICS_LEXICAUX:
for m in re.finditer(motif, texte, flags=re.I):
extrait = re.sub(r"\s+", " ", m.group(0))
ligne = next((i + 1 for i, l in enumerate(lignes) if extrait[:25] in l), 0)
tics.append((libelle, extrait, ligne))
# charnières en tête de paragraphe et de phrase
char_para, char_phrase = [], []
for bloc in blocs:
premiere = bloc.lstrip("*_> ")
m = CHARNIERE_RE.match(premiere)
if m:
ligne = next((i + 1 for i, l in enumerate(lignes) if premiere[:30] in l), 0)
char_para.append((m.group(1), ligne))
for p in phrases(bloc)[1:]:
m2 = CHARNIERE_RE.match(p.lstrip("*_ "))
if m2:
ligne = next((i + 1 for i, l in enumerate(lignes) if p[:30] in l), 0)
char_phrase.append((m2.group(1), ligne))
return {
"fichier": os.path.basename(chemin),
"phrases": len(longueurs),
"moyenne": moyenne,
"ecart": ecart,
"cv": ecart / moyenne if moyenne else 0,
"courtes": courtes,
"pct_courtes": 100 * courtes / len(longueurs),
"courtes_iso": courtes_iso,
"courtes_int": courtes_int,
"pct_courtes_int": 100 * courtes_int / len(longueurs),
"couperets": couperets,
"pct_couperets": 100 * couperets / nb_para if nb_para else 0,
"longues": longues,
"plateaux": plateaux,
"para": len(blocs),
"para_mesures": nb_para,
"para_mono": para_mono,
"para_une_phrase": para_une_phrase,
"tics": tics,
"char_para": char_para,
"char_phrase": char_phrase,
"mots": total_mots,
"tirets": tirets,
"tirets_1000": tirets * par_mille,
"pv_1000": pv * par_mille,
"antitheses": antitheses,
"tricolons": tricolons,
"cv_para": cv_para,
"verbose": verbose,
}
# --- Restitution -------------------------------------------------------------
SEUILS = {
"cv": 0.55, # coefficient de variation des longueurs de phrase
"pct_courtes": 12.0, # part de phrases de 5 mots ou moins (toutes) — INFORMATIF, ne conditionne plus le verdict
"pct_courtes_int": None, # PLANCHER RETIRÉ LE 04/08/2026 PAR ARBITRAGE DE L'AUTEUR (voir ci-dessous)
"pct_couperets": 10.0, # PLAFOND de paragraphes-couperets (<= 12 mots) — MAINTENU, garde-fou anti-hachage
"tirets_1000": 4.0, # tirets cadratins pour 1 000 mots
"antitheses": 6, # « ce n'est pas X, c'est Y » par section
"cv_para": 0.45, # variation de la longueur des paragraphes
}
def verdict(r):
alertes = []
if r["cv"] < SEUILS["cv"]:
alertes.append("rythme trop régulier")
# PLANCHER DE PHRASES BRÈVES LOGÉES : RETIRÉ LE 04/08/2026, ARBITRAGE DE L'AUTEUR.
# Motif : la réécriture v6 par le dispositif Gemini produit une prose qui tient son
# rythme par la subordination et par la période plutôt que par la phrase-scansion,
# et cette prose est voulue. Le déficit mesuré (3 à 8 % contre un plancher de 11 %)
# n'est donc pas un défaut à corriger mais une caractéristique assumée de la v6.
# Application directe de l'encadré en tête de la section 3 de 00-COORDINATION.md :
# quand la mesure et le texte se contredisent, c'est le texte qui a raison, et
# l'écart se note au lieu de se corriger.
# LA MESURE RESTE AFFICHÉE, à titre informatif, pour suivre la dérive d'une passe à
# l'autre. Elle ne produit plus d'alerte. NE PAS LA RÉTABLIR SANS ARBITRAGE.
# Le garde-fou anti-hachage, lui, est conservé : c'est le plafond de couperets.
# Symétrique, et c'est le garde-fou anti-hachage (voir section 3 duodecies) :
# une accumulation de paragraphes d'une ligne casse le fil de lecture.
if r["para_mesures"] >= 20 and r["pct_couperets"] > SEUILS["pct_couperets"]:
alertes.append("hachage : trop de paragraphes-couperets")
if len(r["plateaux"]) > max(3, r["para"] // 12):
alertes.append("plateaux de longueur")
if r["tics"]:
alertes.append("lexique de LLM")
if len(r["char_para"]) > 2:
alertes.append("charnières en tête de paragraphe")
# Les densités pour 1 000 mots n'ont pas de sens sur un texte très court
# (4e de couverture, page de partie) : un seul tiret y pèse 3 points.
if r["mots"] >= 800 and r["tirets_1000"] > SEUILS["tirets_1000"]:
alertes.append("surdose de tirets cadratins")
if len(r["antitheses"]) > SEUILS["antitheses"]:
alertes.append("antithèses « pas X, c'est Y » en série")
if r["cv_para"] < SEUILS["cv_para"]:
alertes.append("paragraphes trop réguliers")
return alertes
def afficher(r):
print(f"\n{'=' * 78}\n{r['fichier']}\n{'=' * 78}")
print(f" Phrases analysées : {r['phrases']} ({r['para']} paragraphes)")
print(f" Longueur moyenne : {r['moyenne']:.1f} mots")
print(f" Écart-type / variation : {r['ecart']:.1f} → CV = {r['cv']:.2f}"
f" (cible ≥ {SEUILS['cv']:.2f})")
print(f" Phrases ≤ 5 mots : {r['courtes']} ({r['pct_courtes']:.1f} %)"
f" (informatif, ancienne cible ≥ {SEUILS['pct_courtes']:.0f} %)")
print(f" · dont logées dans un paragraphe ample : {r['courtes_int']}"
f" ({r['pct_courtes_int']:.1f} %) (INFORMATIF — plancher retiré le 04/08/2026)")
print(f" · dont seules sur leur ligne : {r['courtes_iso']}"
f" (ne comptent pas comme rythme)")
print(f" Phrases ≥ 30 mots : {r['longues']}")
print(f" PARAGRAPHES-COUPERETS (≤ 12 mots) : {r['couperets']}"
f" ({r['pct_couperets']:.1f} %) (PLAFOND {SEUILS['pct_couperets']:.0f} %,"
f" repère chapitres 1-4 : 4,5 à 7,6 %)")
print(f" Paragraphes d'une seule phrase : {r['para_une_phrase']}")
print(f" Paragraphes sans respiration courte (≥3 phrases, toutes > 8 mots) : {r['para_mono']}")
if r["plateaux"]:
print(f" Plateaux rythmiques (≥3 phrases de longueur ±2) : {len(r['plateaux'])}")
if r["verbose"]:
for longueur, n in r["plateaux"][:20]:
print(f" · {n} phrases autour de {longueur} mots")
if r["tics"]:
print(f" TICS LEXICAUX : {len(r['tics'])}")
for libelle, extrait, ligne in r["tics"]:
print(f" · l.{ligne:<5} [{libelle}] « {extrait} »")
else:
print(" Tics lexicaux : aucun")
if r["char_para"]:
print(f" CHARNIÈRES EN TÊTE DE PARAGRAPHE : {len(r['char_para'])}")
for mot, ligne in r["char_para"]:
print(f" · l.{ligne:<5} « {mot} »")
if r["char_phrase"]:
print(f" Charnières en tête de phrase (dans le corps) : {len(r['char_phrase'])}")
if r["verbose"]:
for mot, ligne in r["char_phrase"]:
print(f" · l.{ligne:<5} « {mot} »")
court = " [texte trop court : densité non significative]" if r["mots"] < 800 else ""
print(f" Tirets cadratins : {r['tirets']} ({r['tirets_1000']:.1f} / 1 000 mots"
f" — plafond {SEUILS['tirets_1000']:.0f}){court}")
print(f" Points-virgules : {r['pv_1000']:.1f} / 1 000 mots")
print(f" Antithèses « pas X, c'est Y » : {len(r['antitheses'])}"
f" (plafond {SEUILS['antitheses']})")
if r["verbose"]:
for a in r["antitheses"]:
print(f" · {a}")
print(f" Tricolons (A, B et C) : {r['tricolons']}")
print(f" Variation longueur des paragraphes : CV = {r['cv_para']:.2f}"
f" (cible ≥ {SEUILS['cv_para']:.2f})")
a = verdict(r)
print(f" → VERDICT : {'; '.join(a) if a else 'conforme aux quatre règles de brouillage'}")
def main():
args = [a for a in sys.argv[1:] if not a.startswith("--")]
verbose = "--verbose" in sys.argv or "-v" in sys.argv
if args:
cibles = []
for a in args:
cibles.extend(sorted(glob.glob(a)) or [a])
else:
cibles = [f for f in MANUSCRIT if os.path.exists(f) and os.path.getsize(f) > 1500]
resultats = []
for c in cibles:
if not os.path.exists(c):
print(f"[introuvable] {c}")
continue
r = analyser(c, verbose)
if r:
resultats.append(r)
afficher(r)
if len(resultats) > 1:
print(f"\n{'=' * 78}\nSYNTHÈSE\n{'=' * 78}")
print(f"{'Fichier':<38}{'CV':>6}{'%≤5m':>7}{'%int':>7}{'%coup':>7}"
f"{'Tics':>6}{'Char':>6}{'—/1000':>9}{'Antith':>8}{'CVpara':>8}")
for r in resultats:
print(f"{r['fichier']:<38}{r['cv']:>6.2f}{r['pct_courtes']:>6.1f}%"
f"{r['pct_courtes_int']:>6.1f}%{r['pct_couperets']:>6.1f}%"
f"{len(r['tics']):>6}{len(r['char_para']):>6}"
f"{r['tirets_1000']:>9.1f}{len(r['antitheses']):>8}{r['cv_para']:>8.2f}")
if __name__ == "__main__":
main()