Files
BookIA/structure-epub.lua
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

320 lines
14 KiB
Lua

--[[
structure-epub.lua — filtre Pandoc, appliqué à la SEULE sortie EPUB.
Problème résolu.
Trois éléments de structure du livre sont écrits en LaTeX brut dans le
manuscrit (dossier Manuscrit/, voir 00-COORDINATION.md section 11),
pour contourner des bugs de mise en page du PDF documentés en section
3 octies de 00-COORDINATION.md :
- le titre de l'Introduction (Manuscrit/00-introduction.md) ;
- le titre de la Conclusion (Manuscrit/13-conclusion.md) ;
- les pages de titre de partie (Manuscrit/partieN-*.md).
Un bloc LaTeX brut est purement et simplement ignoré par le
convertisseur EPUB. Résultat, avant ce filtre : dans l'EPUB,
l'Introduction n'avait AUCUN titre, était collée à la fin du fichier
de la préface, et n'apparaissait pas dans le sommaire de la liseuse.
Même chose pour la Conclusion. Les quatre pages de partie ne
provoquaient aucune coupure de fichier.
Ce que fait le filtre : il relit ces blocs LaTeX et en reconstruit un
vrai titre de niveau 1 pour l'EPUB. Pandoc coupe ses fichiers XHTML
aux titres de niveau 1 (--epub-chapter-level, 1 par défaut) et
construit son sommaire à partir d'eux : l'Introduction, la Conclusion
et les quatre parties retrouvent donc d'un coup leur titre affiché,
leur propre fichier, et leur entrée de navigation.
Le manuscrit garde une source unique par élément — le bloc LaTeX —,
ce qui évite de maintenir deux fois le même titre à deux endroits.
Le PDF n'est pas concerné : ce filtre n'est passé qu'à l'appel Pandoc
qui produit l'EPUB (voir compiler-livre.sh).
]]
local part_count = 0
-- Les tirets LaTeX (--- et --) n'ont pas de sens hors du PDF.
local function detex(text)
return (text:gsub("%-%-%-", "\u{2014}"):gsub("%-%-", "\u{2013}"))
end
-- Introduction / Conclusion / Préface : \chapter*{\texorpdfstring{
-- \eyebrowtitle{Introduction}Le silence après la question}{...}}.
-- Le PREMIER argument de \texorpdfstring (celui réellement composé
-- dans le PDF, voir preambule.tex section 2 bis, \eyebrowtitle) porte
-- désormais le surtitre séparé du titre — on le reprend pour l'EPUB
-- et on reconstruit le MÊME rendu à deux composants que les pages de
-- partie (span de surtitre + titre), plutôt que le texte à plat du
-- second argument, pour que le résultat EPUB corresponde visuellement
-- à ce qui est composé dans le PDF (surtitre discret, titre en
-- dessous), et pas seulement à son équivalent en une seule ligne.
--
-- Séparateur harmonisé le 27/07/2026 (remarque de l'auteur) : le
-- second argument de \texorpdfstring (« Introduction — Le silence
-- après la question ») utilise un tiret cadratin, incohérent avec le
-- deux-points déjà retenu pour les parties (« Partie 1 : L'Érosion du
-- Réel », voir plus bas). Le rendu EPUB de ce span ignore donc ce
-- second argument et compose son propre séparateur en deux-points,
-- pour que Introduction, Conclusion, Préface et les quatre parties
-- partagent tous la même convention dans le sommaire de la liseuse.
-- Le second argument continue de servir uniquement aux signets PDF,
-- où le tiret cadratin reste la convention voulue par l'auteur pour
-- ces trois éléments (voir preambule.tex, contrainte non modifiée).
local function inlines_from_eyebrow_chapter(text)
-- Le RawBlock complet contient plusieurs lignes LaTeX à la suite
-- (\chapter*{...} puis \label{...}, \addcontentsline{...} ou
-- \markright{...}) : on isole la seule ligne \chapter*{...} avant
-- toute extraction, pour ne jamais capturer au-delà (bug déjà
-- rencontré et corrigé le 27/07/2026 sur l'ancienne version de
-- cette fonction).
local line = text:match("^[^\n]*\\chapter%*[^\n]*")
if not line then
return nil
end
local surtitle, main = line:match("\\eyebrowtitle{(.-)}(.-)}{")
if not surtitle or main == "" then
return nil
end
return pandoc.Inlines{
pandoc.Span(
pandoc.Inlines{pandoc.Str(surtitle .. "\u{00A0}:")},
pandoc.Attr("", {"part-number"})
),
pandoc.Space(),
pandoc.Str(detex(main)),
}
end
local function header(inlines, classes)
return pandoc.Header(1, inlines, pandoc.Attr("", classes))
end
-- Numérotation des chapitres dans le sommaire EPUB (nav.xhtml) et dans
-- le titre de chaque fichier XHTML. Ajoutée le 03/08/2026, pour que
-- l'EPUB reste cohérent avec le PDF (preambule.tex, section 4 : le
-- numéro de chapitre apparaît désormais dans la table des matières).
--
-- Cible uniquement les titres de CHAPITRE numérotés, qui restent du
-- Markdown ordinaire (`# Titre`, aucun bloc LaTeX brut) : ce sont donc
-- de vrais Header au moment où pandoc parse le fichier, contrairement
-- aux titres de partie/introduction/conclusion, qui n'existent QUE
-- comme RawBlock jusqu'à ce que la fonction RawBlock ci-dessus les
-- transforme elle-même en Header. En traversée typewise (le mode par
-- défaut de pandoc, non modifié ici), chaque nœud n'est soumis qu'à
-- la fonction correspondant à SON type dans l'AST d'origine : un nœud
-- qui était un RawBlock ne repasse pas ensuite par la fonction
-- Header, même une fois transformé. Cette fonction ne voit donc que
-- les 12 titres de chapitre réels, jamais les 4 titres de partie, ni
-- l'introduction, ni la conclusion — aucun garde-fou supplémentaire
-- n'est nécessaire, mais on vérifie tout de même l'absence des
-- classes "unnumbered"/"part-title" par défense, au cas où un futur
-- chapitre serait un jour construit via un bloc LaTeX brut lui aussi.
local chapter_count = 0
function Header(el)
if el.level == 1
and not el.classes:includes("unnumbered")
and not el.classes:includes("part-title") then
chapter_count = chapter_count + 1
el.content = pandoc.Inlines{
pandoc.Span(
pandoc.Inlines{pandoc.Str(chapter_count .. ".")},
pandoc.Attr("", {"chapter-number"})
),
pandoc.Space(),
} .. el.content
end
return el
end
function RawBlock(el)
if el.format ~= "latex" and el.format ~= "tex" then
return nil
end
-- Page de titre de partie : \part{Titre}
local part = el.text:match("\\part%s*{(.-)}")
if part then
part_count = part_count + 1
-- « Partie N » va dans un span pour que la feuille de style puisse
-- le composer en plus petit, au-dessus du titre, comme dans le PDF.
-- Le sommaire de la liseuse, qui ne retient que le texte du span
-- (pas le CSS ni le display:block qui isole « Partie N » sur sa
-- propre ligne à l'écran), affiche donc « Partie N » et le titre
-- accolés dans le même texte plat : il faut un séparateur textuel
-- explicite entre les deux.
-- Séparateur choisi : le deux-points, PAS un tiret cadratin.
-- Ce n'est pas une convention inventée pour l'EPUB : c'est celle
-- déjà écrite par l'auteur dans 00-COORDINATION.md pour désigner
-- chaque partie du plan du livre (« Partie 1 : L'Érosion du Réel »,
-- « Partie 2 : Le Grand Bouleversement »...). Le tiret cadratin,
-- lui, est la convention réservée à l'Introduction/Conclusion
-- (« Introduction — Le silence après la question »), déjà présente
-- telle quelle dans le manuscrit (\texorpdfstring, voir plus haut).
-- Espace insécable avant le deux-points, comme partout ailleurs
-- dans ce livre en typographie française (le filtre
-- typographie-francaise.lua fait ce travail sur le corps du texte
-- ; ce titre est construit par un autre filtre, donc directement
-- en dur ici).
--
-- Numérotation romaine ajoutée le 03/08/2026, pour que l'EPUB
-- reste cohérent avec le PDF (preambule.tex, section 0 :
-- \thepart = \Roman{part}). pandoc.utils.to_roman_numeral produit
-- directement la forme majuscule attendue (I, II, III, IV...).
return header(
pandoc.Inlines{
pandoc.Span(
pandoc.Inlines{pandoc.Str(
"Partie " .. pandoc.utils.to_roman_numeral(part_count) .. "\u{00A0}:"
)},
pandoc.Attr("", {"part-number"})
),
pandoc.Space(),
pandoc.Str(detex(part)),
},
{ "part-title", "unnumbered" }
)
end
-- Introduction / Conclusion / Préface :
-- \chapter*{\texorpdfstring{\eyebrowtitle{A}B}{A — B}}
if el.text:match("\\chapter%*%s*{\\texorpdfstring{") then
local inlines = inlines_from_eyebrow_chapter(el.text)
if inlines then
-- La préface n'a jamais eu de \addcontentsline{toc}{part}{...}
-- (contrairement à l'Introduction et à la Conclusion) : c'est
-- déjà ainsi qu'elle était marquée « absente du sommaire » avant
-- ce changement de structure (via la classe .unlisted de
-- Markdown, qui n'existe plus une fois le titre passé en LaTeX
-- brut). On préserve ce même comportement en testant la
-- présence de \addcontentsline dans le même bloc : absent ->
-- unlisted, comme l'était la préface ; présent -> visible dans
-- le sommaire, comme le sont l'Introduction et la Conclusion.
local classes = { "unnumbered" }
if not el.text:match("\\addcontentsline") then
table.insert(classes, "unlisted")
end
return header(inlines, classes)
end
end
-- Repli : le titre tel qu'il est inscrit dans la table des matières.
local toc = el.text:match("\\addcontentsline%s*{toc}%s*{part}%s*{(.-)}")
if toc then
return header(pandoc.Inlines{pandoc.Str(detex(toc))}, { "unnumbered" })
end
-- Marqueur EPUB-ONLY-TITLE : un commentaire LaTeX (`%`), donc muet
-- pour le PDF, qui porte un titre à composer UNIQUEMENT pour l'EPUB.
-- Cas d'usage : une page qui a déjà son rendu PDF fini (mentions
-- légales, page nue sans titre) mais qui, sans aucun titre de
-- niveau 1 dans le fichier .md, se voyait attribuer par Pandoc le
-- titre du livre entier comme repli — créant une entrée fantôme
-- dans le sommaire EPUB et le fichier NCX (bug constaté le
-- 27/07/2026 sur mentions-legales.md). Non numéroté et absent du
-- sommaire, comme la page qui porte le sommaire lui-même (voir
-- table-des-matieres.md, anciennement preface.md).
local epubOnly = el.text:match("^%%%s*EPUB%-ONLY%-TITLE:%s*(.-)%s*$")
if epubOnly then
return header(pandoc.Inlines{pandoc.Str(epubOnly)}, { "unnumbered", "unlisted" })
end
return nil
end
--[[
Notes de bas de page conformes à la page d'aide KDP « Règles
relatives aux notes de bas de page » (revue de conformité KDP,
27/07/2026) — implémentation LITTÉRALE de l'exemple de code fourni
par cette page, « Méthode 1 (recommandée) » :
<p>...<sup><a id="source" href="#ft-1-1"
epub:type="noteref">1</a></sup></p>
...
<aside id="ft-1-1" epub:type="footnote">
<p><a epub:type="noteref" href="#source">1.</a> Il s'agit de la
note de bas de page, qui doit être placée à la fin du chapitre
ou du livre.</p>
</aside>
Ce que cet exemple prescrit, et rien de plus :
- un hyperlien BIDIRECTIONNEL (texte -> note, note -> texte) —
c'est l'exigence, formulée ailleurs sur la même page : « Amazon
exige de mettre en forme les notes de bas de page en utilisant
des hyperliens bidirectionnels » ;
- le lien de retour est composé du NUMÉRO SUIVI D'UN POINT
(« 1. »), pas d'une flèche ni d'un symbole séparé ;
- ce lien de retour se place EN TÊTE du même paragraphe que le
texte de la note, pas dans un paragraphe séparé ;
- le lien de retour porte le même attribut epub:type="noteref"
que l'appel dans le corps du texte — pas de classe ni d'attribut
inventés (une version précédente de ce filtre posait une classe
"footnote-back" et un attribut title absents de cet exemple :
retirés, pour suivre l'exemple KDP à la lettre).
Constaté par test direct sur ce projet : Pandoc 3.9.0.2 pose bien
l'appel dans le corps du texte (`<a href="#fnN" ...
epub:type="noteref">N</a>`), mais NE POSE PAS le lien de retour dans
sa sortie EPUB — ce que cette section du filtre corrige.
Contraintes techniques rencontrées et résolues (mécanique interne,
sans rapport avec le contenu de la règle KDP elle-même) :
1. Au moment où les filtres Lua s'exécutent, une note de bas de page
est encore un élément Note de l'AST Pandoc — la transformation en
<aside id="fnN"> avec son appel <a id="fnrefN"> n'a lieu que plus
tard, à l'écriture, hors de portée d'un filtre Div ou Link classique
(vérifié par sonde). On intercepte donc Note() et on construit
nous-mêmes le paragraphe de retour, en PRÉDISANT l'identifiant que
Pandoc attribuera à l'appel correspondant (fnref<N>).
2. La numérotation des notes REDÉMARRE À 1 à chaque fichier XHTML
produit par l'EPUB (Pandoc découpe un nouveau fichier à chaque titre
de niveau 1) : un compteur global désynchronise donc les liens dès
le deuxième chapitre. Un compteur remis à zéro dans un handler
Header() séparé ne suffit pas non plus : vérifié par sonde, Pandoc
ne visite pas les handlers Header() et Note() dans l'ordre du
document quand ils sont déclarés comme deux fonctions globales
distinctes (tous les Note() du document sont visités avant tous les
Header()). La solution retenue pilote donc l'ordre explicitement :
un seul point d'entrée Pandoc(doc), qui parcourt doc.blocks lui-même
dans l'ordre réel du document, remet le compteur à zéro à chaque
Header de niveau 1 rencontré, puis descend dans CE bloc précis
(pandoc.walk_block) pour numéroter les Note qu'il contient.
]]
local note_count = 0
local function number_notes_in_block(block)
return pandoc.walk_block(block, {
Note = function(el)
note_count = note_count + 1
-- Lien de retour "N." — même forme que l'exemple KDP, posé en
-- tête du premier paragraphe de la note, immédiatement suivi du
-- texte existant de la note (qui commence par un espace, comme
-- dans l'exemple KDP : « 1. Il s'agit de la note... »).
local backlink = pandoc.Link(
pandoc.Inlines{pandoc.Str(note_count .. ".")},
"#fnref" .. note_count,
"",
pandoc.Attr("", {}, {["epub:type"] = "noteref"})
)
el.content[1] = pandoc.Para(
pandoc.Inlines{backlink, pandoc.Space()} .. el.content[1].content
)
return el
end
})
end
function Pandoc(doc)
local new_blocks = pandoc.Blocks{}
for _, block in ipairs(doc.blocks) do
if block.t == "Header" and block.level == 1 then
note_count = 0
end
new_blocks:insert(number_notes_in_block(block))
end
doc.blocks = new_blocks
return doc
end