Générer un Roman Illustré avec LLM et IA Générative
Générer un Roman Illustré avec LLM et IA Générative
Peut-on confier la rédaction d’un roman de 30 000 mots à plusieurs LLM travaillant en parallèle, valider chaque section par une revue multi-modèles, puis générer les illustrations des personnages avec un modèle de diffusion — le tout piloté par du code Python ? C’est exactement ce que nous avons fait avec le projet Lorraine Novel Generator, et voici ce que ça donne en production.
Selon une étude de 2024 de l’Université de Stanford, les LLM atteignent la cohérence stylistique humaine sur des textes courts, mais se dégradent significativement au-delà de 5 000 tokens de contexte. Notre défi : écrire Les Enfants du Fer, 105 ans d’histoire industrielle lorraine, en maintenant une cohérence narrative sur 45 sections et 4 versions parallèles du même récit.
L’architecture multi-versions : un même matériau, quatre lectures
Le roman existe en 4 versions narratives du même matériau historique :
| Version | Sous-titre | Sections | Mots | Structure |
|---|---|---|---|---|
| 1 | Ce qu’on ne voit pas | 15 | ~9 857 | POV alternés + interludes IA |
| 2 | Cinq générations, même sol | 5 | ~7 231 | Chronologique 1940→2045 |
| 3 | Les Dieux d’en bas | 10 | ~7 518 | Chapitres + interludes encyclopédiques |
| 4 | Trois Voix pour un puits | 15 | ~8 129 | Triades fable/poème/interlude |
Chaque version est un fichier story.json distinct, et chaque section est un fichier JSON indépendant :
{
"id": "01a_fable_terre_occupee",
"story_id": "enfants_du_fer_trois_voix",
"title": "FABLE — 1940 · La terre occupée",
"type": "fable",
"order": 1,
"target_words": 1200,
"status": "written",
"word_count": 1156,
"segments": [
{"type": "telluric", "text": "..."},
{"type": "narration", "text": "..."},
{"type": "dialogue", "speaker": "Heinrich", "text": "..."},
{"type": "verse", "text": "..."}
]
} Ce format unifié permet à 7 renderers de convertir le même contenu vers Markdown, Fountain (scénario), script BD franco-belge, RSS podcast, prompts musicaux Suno, prompts images Midjourney, et prompts vidéo Runway — sans modifier une seule ligne de texte.
La bible narrative : le vrai travail de fond
Avant d’écrire une seule ligne de roman, nous avons produit :
- 57 000 caractères de recherche historique :
contexte_historique_lorrain.mdcouvre les houillères, la minette, les travailleurs frontaliers et la transition vers l’hydrogène, de 1810 à 2050 - La bible narrative (
bible_v8.md) : 7 chapitres + 8 interludes, chronologie sur 10 ans, trois personnages (Diane, Adam, Nora), règles d’écriture strictes — pas de gras, cliffhangers systématiques, mots-cibles par section - Les recettes Werber : philosophie-fiction, narration multi-fils, alternance frustration/satisfaction, inserts encyclopédiques intégrés
C’est cette bible qui garantit la cohérence entre les 4 versions. Sans elle, chaque LLM réinventerait les personnages à sa guise.
La génération de structure : generate_structure.py
Avant d’écrire le moindre paragraphe, le script tools/generate_structure.py génère le squelette complet des 4 histoires. Chaque section est créée comme un fichier JSON vide avec des métadonnées précises :
def make_section(id_, story_id, title, type_, order, target_words, brief="", notes=None):
return {
"id": id_,
"story_id": story_id,
"title": title,
"type": type_,
"order": order,
"target_words": target_words,
"brief": brief,
"notes": notes or [],
"status": "empty",
"word_count": 0,
"segments": []
} Le champ notes est particulierement important : il contient les contraintes narratives par section. Par exemple, pour le chapitre 2 de la version 9, les notes imposent : ["POV Adam", "Question d'enfant : Mila 6 ans, descendaient sous la terre", "Cliffhanger : Marc leve son verre, Adam ne rit pas"]. Ces contraintes sont injectees dans le prompt du LLM pour chaque section, garantissant que le narrateur, les questions d’enfants (un motif recurrent du roman) et les cliffhangers sont respectes.
La version 9 (la plus aboutie) suit une structure alternee stricte : 7 chapitres de 1050 mots en POV alternes (Diane, Adam, Nora) entrelaces avec 8 interludes de 300 mots prononces par une voix mysterieuse (que le lecteur decouvre etre une IA dans le dernier interlude). Chaque interlude couvre un theme industriel lorrain : le fer, le charbon, l’hydrogene, les frontaliers et l’IA, l’acier vert, la souverainete, la machine, et enfin la revelation.
Le processus d’écriture hybride multi-modèles
Nous avons délibérément choisi trois LLM de régions différentes pour maximiser la diversité des retours :
- Mistral (Europe) — garant de la sensibilité culturelle française
- Gemini (USA) — pour la structure narrative américaine
- DeepSeek (Chine) — pour les perspectives longues durée
Chaque section passe par le script tools/review.py qui interroge les trois modèles et retourne un rapport de cohérence. Le processus est asynchrone : l’humain valide ou rejette les suggestions avant de passer à la section suivante.
Pour le brainstorming rapide, tools/mammouth.py est un wrapper ultra-low-cost qui utilise l’API Mammouth (un routeur multi-modeles compatible OpenAI). Le fichier expose une table de 10 modeles tries par cout :
MODELS = {
# Ultra-cheap (< 0.15 in)
"nano": "gpt-4.1-nano", # $0.10/$0.40
"mistral-s": "mistral-small-3.2-24b-instruct", # $0.10/$0.30
"scout": "llama-4-scout", # $0.15/$0.60
# Cheap (0.20-0.50 in)
"grok": "grok-4-fast", # $0.20/$0.50
"maverick": "llama-4-maverick", # $0.22/$0.88
"deepseek": "deepseek-v3.2", # $0.30/$0.45
"gemini": "gemini-3-flash-preview", # $0.30/$2.50
"mini": "gpt-4.1-mini", # $0.40/$1.60
"mistral-l": "mistral-large-3", # $0.50/$1.50
# Premium
"glm": "glm-5", # $0.95/$2.55
} Chaque modele est accessible via ask(question, model="nano") ou brainstorm(preset, input_text). Le systeme de presets integre est concu pour l’ecriture : sensory genere des images sensorielles (odeur, toucher, son), closing propose des phrases de cloture ambigues, historical fournit des details factuels lorrains. Le preset par defaut impose un style “Werber x Zola x Giono, Lorraine industrielle, 100 mots max”.
L’appel est fait via curl en subprocess (pas de SDK) pour minimiser les dependances, avec un timeout de 90 secondes et un log systematique du cout par appel sur stderr :
print(f" [mammouth] {model_id} | {tokens_in}->{tokens_out} tokens | ${cost:.4f}", file=sys.stderr) En pratique, une session de brainstorming de 30 iterations sur gpt-4.1-nano coute environ $0.12 — soit 50 fois moins qu’un seul appel GPT-4.
Les character sheets : cohérence visuelle des personnages
Un problème classique de la génération d’illustrations : le même personnage change de visage à chaque image. Notre solution est le script tools/character_sheet_gen.py qui génère des fiches personnages multi-vues via l’API Runware :
- Vue face, dos, profil gauche, profil droit, trois-quarts
- Même prompt de base + modificateur de vue
- Sauvegarde de l’image de référence pour seed les générations suivantes
Le script supporte 6 modeles de generation d’images via l’API Runware, tries par cout :
| Modele | Cout/image | Capacite |
|---|---|---|
qwen-image | $0.006 | Text-to-image uniquement |
flux-dev | $0.003 | Img2img via seedImage |
kontext | $0.011 | FLUX.1 Kontext, meilleure coherence multi-vues |
gpt-image-low | $0.013 | Img2img via referenceImages |
grok | $0.020 | Grok Imagine (Aurora) |
nano-banana | $0.039 | Google Nano Banana, meilleure qualite |
Le pipeline type pour une fiche personnage complete : text-to-image avec grok pour la vue FRONT ($0.02), puis 3 vues img2img avec kontext en utilisant la vue FRONT comme reference ($0.033). Cout total par personnage : ~$0.06 pour 4 vues coherentes.
Le mode --analyze pousse la validation plus loin : chaque image generee est envoyee a un VLM (Qwen3-VL 30B sur Ollama local, ou DeepInfra en fallback) qui evalue la coherence du personnage avec la description textuelle. Le mode --sam3 ajoute une detection SAM3 du personnage, du visage et de la bouche — ces masques sont reutilises ensuite pour le placement de bulles dans le pipeline BD.
Ce n’est pas parfait — la coherence absolue reste un probleme ouvert dans la diffusion — mais les 4 vues permettent d’entrainer l’oeil du lecteur sur les traits distinctifs de chaque personnage.
Le dashboard Gradio : lire et assembler le roman
L’interface app.py est un dashboard Gradio qui charge toutes les histoires depuis romans/ et gère le rendu HTML par type de segment :
def segments_to_html(segments):
"""Convertit les segments en HTML stylisé."""
for seg in segments:
t = seg.get("type", "narration")
if t == "telluric":
parts.append(
f'<div style="background:#2a2418; color:#e8d5a8; padding:14px 18px; '
f'border-left:4px solid #c4a35a; ...">{text}</div>'
)
elif t == "dialogue":
parts.append(
f'<div style="margin:8px 0 8px 28px;">'
f'<span style="color:#f4a948; font-weight:700;">{speaker}</span>'
f' — <em>{text}</em></div>'
)
elif t == "verse":
parts.append(
f'<div style="text-align:center; color:#d4b8ff; '
f'font-style:italic; line-height:2.2;">{text}</div>'
) Chaque type de segment a sa propre typographie : les segments telluric (voix de la terre) sur fond sombre avec bordure dorée, les dialogues en retrait avec le nom du locuteur coloré, les vers centrés en violet pâle.
Le pipeline BD : bulles et physique de collision
Le sous-module tools/bd_page_gen.py + bubble_placement_test.py est la pièce la plus complexe : générer des planches BD complètes avec placement physique des bulles. Le problème des bulles en BD n’est pas trivial — une bulle ne doit pas masquer un visage, deux bulles ne doivent pas se chevaucher, et la queue de la bulle doit pointer vers le personnage qui parle.
Notre approche : simulation de collision par cercles englobants. Chaque bulle est représentée comme un cercle avec un rayon proportionnel au texte. On itère par force répulsive entre bulles jusqu’à stabilisation, puis on valide que chaque bulle pointe vers le bon personnage.
L’export multi-format : export_book.py
Le script tools/export_book.py exporte les 4 histoires en ePub, PDF et PPTX. Un detail souvent neglige dans les projets de generation : la typographie francaise. Le script applique automatiquement 5 corrections typographiques a l’export (sans modifier les fichiers JSON source) :
def _typo_fr(text: str) -> str:
# 1. Apostrophes droites -> typographiques (U+2019)
text = text.replace("'", "’")
# 2. Points de suspension ... -> ... (U+2026)
text = text.replace("...", "…")
# 3. Doubles espaces -> simple
text = _re.sub(r" +", " ", text)
# 4. Guillemets droits "texte" -> << texte >> (avec insecables)
text = _re.sub(r'"([^"]+)"', lambda m: "<< " + m.group(1) + " >>", text)
# 5. Espaces insecables avant : ; ! ? >>
text = _re.sub(r" ([;:!?>>])", " \1", text) C’est un detail invisible pour le lecteur, mais qui fait la difference entre un livre genere par IA et un livre publiable. Les espaces insecables avant la ponctuation double sont une regle typographique francaise que les LLM ne respectent pas systematiquement.
Ce que nous avons appris
Le schéma universel est la vraie valeur ajoutée. Le novel_schema.json (2 160 lignes, JSON Schema 2020-12) définit trois couches : données, atmosphère (humeur, visuel, audio, sensoriel par bloc), et render hints (directives par format de sortie). Ce schéma est la colonne vertébrale qui permet à 7 renderers de produire des sorties cohérentes à partir du même JSON.
La cohérence narrative ne vient pas du LLM, elle vient de la bible. Le LLM est un exécutant très capable, mais c’est la bible qui garantit que Diane est la même Diane dans les 4 versions, que la chronologie est respectée, que les mots-cibles sont atteints.
Le multi-modèle est utile pour la diversité, pas pour la consensus. Les trois LLM divergent souvent sur le ton et la structure. C’est voulu : on prend le meilleur de chacun, pas la moyenne.
Au final : 4 histoires terminées, 45 sections, ~32 700 mots. Une infrastructure réutilisable pour tout projet de création littéraire assistée par IA.
Le moteur de revue : parallélisme et coût maîtrisé
Le script tools/review.py révèle une contrainte économique souvent sous-estimée : évaluer 45 sections × 3 modèles en séquentiel prendrait des heures. La solution est un ThreadPoolExecutor avec 3 workers — un par modèle — qui envoie toutes les requêtes simultanément :
SYSTEM = """Tu es un éditeur littéraire français exigeant. Roman philosophie-fiction,
style Werber×Zola×Giono, narrateur=territoire (le sol perçoit, ne juge pas).
Pour ce chapitre complet :
1. Les 3 clichés les plus gênants (cite le passage exact, propose une alternative)
2. Les 2 phrases les plus faibles (cite, réécris)
3. Les 2 manques sensoriels (quel sens, où l'ajouter)
Sois chirurgical. 150 mots max."""
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool:
futures = {pool.submit(review_chapter, p, m): (p, m) for p, m in tasks} Autre optimisation : chaque chapitre est tronqué à 2 000 caractères pour la revue. Ce n’est pas idéal — certains passages en fin de chapitre échappent à l’analyse — mais ça réduit le coût de revue de 70% tout en capturant les problèmes les plus visibles (début de chapitre, introduction des personnages, cliffhangers).
Le wrapper tools/mammouth.py pousse cette logique plus loin : pour le brainstorming d’idées (pas la revue finale), il bascule automatiquement vers les modèles nano de chaque famille. Le coût d’une session de brainstorming tombe de ~3€ à ~0,30€.
Les 7 renderers : un JSON, sept formats
Le format JSON universel des sections permet à 7 renderers de produire des sorties radicalement différentes sans toucher au texte :
| Renderer | Format de sortie | Usage |
|---|---|---|
render_markdown | .md Markdown | Lecture directe, publication web |
render_fountain | .fountain | Scénario cinéma/série |
render_bd | HTML panels + bulles | Script BD franco-belge |
render_rss | .xml RSS | Podcast narratif |
render_music | JSON prompts Suno | Musique générative |
render_image | JSON prompts Midjourney | Illustrations par section |
render_video | JSON prompts Runway | Vidéo générative |
Ce n’est pas de la magie — c’est de la traduction de types. Un segment "type": "verse" devient une strophe en Markdown, une didascalie en Fountain, et un prompt musical en mode Suno. La difficulté est dans les segments hybrides : un "type": "dialogue" avec "speaker": "Heinrich" doit devenir une réplique scénaristique avec action de personnage, pas juste une ligne de texte.
Problèmes de cohérence narrative : ce que les LLM ne voient pas
Sur 45 sections et 4 versions parallèles, trois problèmes récurrents sont apparus :
La dérive temporelle. Diane a 34 ans en 1978 dans la version 1, 31 ans dans la version 3. Les LLM ne maintiennent pas automatiquement l’arithmétique temporelle sur des contextes longs. Solution : la bible narrative contient une frise chronologique explicite que chaque prompt inclut systématiquement.
L’inflation des détails sensoriels. Le style Werber recommande des inserts encyclopédiques. Mais sur 15 sections, les LLM ont tendance à répéter les mêmes détails (l’odeur de la mine, la chaleur des hauts-fourneaux) plutôt que d’explorer la variété sensorielle. La revue multi-modèles détecte ces répétitions — c’est précisément l’un des 3 critères du prompt éditeur.
L’incohérence des dialogues traduits. Certains personnages (les travailleurs italiens ou polonais de la mine) parlent un français approximatif dans la version 1, mais un français parfait dans la version 2. Le LLM ne se souvient pas des conventions stylistiques établies dans les sections précédentes. La bible narrative documente maintenant chaque idiolecte de personnage explicitement.
Vous travaillez sur un projet de génération de contenu long format avec LLM ? Contactez-nous — nous avons l’expérience des architectures multi-modèles et des pipelines de validation narrative.