Article → Bande Dessinée (PDF)
Utilisez ceci lorsque l'utilisateur fournit un article (PDF, LaTeX, texte) et demande une bande dessinée, une histoire, un « magazine comique », un explicateur cartoon ou un résumé visuel amusant. Le résultat est un PDF A4, d'environ 8 pages, entièrement dessiné en code (SVG + HTML → Chromium PDF). Aucune génération d'image n'est nécessaire.
1. Lire l'article et créer une fiche de faits (dans votre tête ou un fichier de travail)
Extrayez, en mots simples :
- Le système / dispositif / algorithme étudié par l'article (cela devient le héros).
- Le problème ou la métrique négative (panne, erreur, délai, attaque...) — cela devient le méchant.
- La méthode de base / classique et la méthode proposée (et toute variante, par ex. UCB, ablation).
- L'idée clé, le théorème ou l'intuition, et une métaphore de la vie quotidienne (pièces de puzzle, portes, clés, routes...).
- La règle de décision ou le mécanisme, s'il y en a (seuils, commutateurs, portes).
- La configuration de test (paramètres, nombre d'essais, SNR...).
- Les chiffres clés, les tableaux, les vérifications de robustesse, les limites, les travaux futurs, les auteurs et l'affiliation.
Règles : copiez les nombres exactement comme dans l'article. N'inventez jamais de résultats, de noms de jeux de données ou d'affirmations. Si une valeur simple peut être recalculée (par exemple une dimension ou le résultat d'une formule), recalculez-la avec Python pour vérifier. Conservez les termes propres à l'article mais expliquez chaque acronyme une fois.
2. Distribuer les rôles (fonctions du moteur)
| Rôle dans l'histoire | Fonction du moteur | Utilisation typique |
|---|---|---|
| Héros (le système) | robot(...) | couleur, label sur la poitrine, antenna=True dessine une rampe de ports sur sa tête |
| Mentor / narrateur | scientist(...) | explique la théorie ; headwear="hijab" ou "hair" ; point=True |
| Méchant (le problème) | monster(...) | label sur le ventre, humeurs angry / hurt |
| Méthode de base / classique | sergeant(...) | discipliné, règle, label par ex. "GRID", "BP", "ZF" |
| Méthode proposée / adaptative | explorer(...) | faisceau de lampe torche ; deuxième variante avec tool="lantern" et une autre couleur |
| Règle de décision / seuil | gatekeeper(...) | texte sur plaque label + sub (par ex. "D","99") |
Donnez au héros un nom court et amical lié au sujet (FLUI pour une antenne fluide, DECO pour un décodeur...). Choisissez un titre de bande dessinée accrocheur (par ex. "LES 50 PORTES").
3. Storyboard (arc par défaut de 8 pages — adaptez à l'article)
- Couverture : grand titre, sous-titre "Comment <héros> a appris à ...", tagline avec la leçon principale, tous les personnages, ligne de crédit avec le titre complet de l'article, les auteurs, l'affiliation.
- Ch.1 Le monde et le problème : qui est le héros, ce qu'il doit faire, ce qui le limite (budget, bruit, une chaîne RF...), le méchant. Un petit schéma avec le modèle physique.
- Ch.2 Deux façons de voir : méthode de base vs méthode proposée, comment chacune fonctionne (une formule dans une légende
math), et les règles du jeu équitables de la comparaison. - Ch.3 Le rebondissement / l'intuition clé : une surprise (où la nouvelle idée échoue ou gagne), puis le mentor explique la théorie avec un tableau noir et un tableau.
- Ch.4 Le mécanisme : cases de métaphore (par exemple pièces de puzzle), chiffres de preuve, la règle de décision sous forme de diamant avec branches OUI/NON.
- Ch.5 Le grand test : légende de configuration, tableau des scores, carte de chaleur ou graphique, résultat principal dans une bulle de mentor.
- Ch.6 Robustesse : autres paramètres, vérifications de sensibilité, le "seul échec" expliqué honnêtement.
- Finale : case de célébration, boîte LA LEÇON, limites honnêtes + travaux futurs, AIDE-MÉMOIRE (chiffres clés) et MOTS À CONNAÎTRE (glossaire).
Un article court peut nécessiter 6 pages ; un long jusqu'à 10. Chaque page de chapitre a 4 à 6 cases.
Règles d'écriture :
- Anglais simple (niveau B2), amical, fluide : chaque case répond à la question de la précédente et soulève la suivante.
- Une idée par case. Bulles d'au maximum ~30 mots ; légendes d'au maximum ~35 mots.
- Gras (
<b>) pour les mots et nombres clés. Formules uniquement dans les légendesC(..., cls="math"), avec<sub>/<sup>. - Chaque nombre doit pouvoir être retracé jusqu'à l'article. Marquez les dessins illustratifs comme "typique" ou "exemple".
- Les bulles du mentor utilisent
cls="mentor"; les exclamations utilisentcls="shout".
4. Construction
- Écrivez le moteur ci-dessous dans
comic_engine.pydans un dossier de travail (copiez-le exactement). - Écrivez
story.pyqui importe le moteur, construit les pages et appellerender(pages, "<Title>_Comic_v1.pdf"). - Exécutez-le.
renderécrit également des feuilles d'aperçu PNG 2-up dans<name>_preview/.
Prérequis : Python avec playwright (Chromium), numpy, PIL, pdftoppm (poppler). Les polices Bangers + Comic Neue sont récupérées une fois par npm pack @fontsource/... dans COMIC_FONT_DIR (par défaut /tmp/comic_fonts) ; si hors ligne, le moteur utilise les polices système en secours. Si le Chromium de Playwright est manquant, exécutez python -m playwright install chromium.
Aide-mémoire de mise en page :
- Page : A4 210 × 297 mm. Zone utilisable x = 7…203 mm, y = 16…289 mm (en-tête au-dessus). Gouttière 4 mm. Case pleine largeur w = 196 ; demi-largeur w = 96 (x = 7 et x = 107).
- À l'intérieur d'une case, unités SVG = 4 par mm : une case de 96 × 70 mm fait 384 × 280 unités. Gardez tout l'art à l'intérieur (x < w·4, y < h·4).
- Les personnages sont dessinés avec les pieds à (x, y) ; placez les pieds ≈ h·4 − 8. Hauteurs à s = 1 : robot ≈ 125 unités, scientifique ≈ 120, explorateur ≈ 120, sergent ≈ 115, gardien ≈ 195 ; le monstre est centré et fait environ 150 de large.
- Les bulles/légendes utilisent des mm relatifs à la case. À 9.8 pt, une bulle de 45 mm de large contient ~8 mots par ligne ; chaque ligne fait environ 4.3 mm de haut.
- Queues :
bl br bc tl tr l r— la queue doit pointer vers le locuteur. - Tableaux et boîtes :
RAW(x, y, w, table(...)), ouRAW(..., '<div class="box lesson"><div class="lt">THE LESSON</div>...</div>'); les boîtes avec<table>créent un aide-mémoire, avec<p>un glossaire. - Texte de bannière à l'intérieur d'une case :
'<div class="banner">LE GRAND TEST</div>'comme un élément.
Petit exemple (une page) :
from comic_engine import *
p = []
p.append(panel(7, 16, 196, 70,
robot(160, 272, 1.75, "happy") + dot_row(330, 80, 420, n=50, glow=31, h=26),
[C(3, 3, 74, "Dans une ville pleine d'ondes radio vit <b>FLUI</b>..."),
B(100, 44, 92, "J'ai <b>50 ports</b> mais seulement <b>une</b> chaîne radio !", "tl")]))
p.append(panel(7, 90, 96, 70, scientist(78, 270, 1.4, point=True) + monster(290, 110, 1.05),
[B(3, 3, 50, "Choisissez un port faible et le <b>Monstre des Pannes</b> gagne.", "br", "mentor"),
B(42, 50, 50, "En dessous du seuil ? Vous êtes À MOI !", "tr", "shout")]))
pages = [cover("LES 50 PORTES", "Comment une antenne à une oreille a appris <i>quand</i> regarder intelligemment", art,
"Regardez là où ça compte...<br>mais seulement quand ça compte !", credit_html),
page(2, "Chapitre 1 · Une Oreille, Cinquante Portes", p)]
render(pages, "The_50_Doors_Comic_v1.pdf")
5. Vérification visuelle (toujours)
Ouvrez chaque feuille d'aperçu avec l'outil de lecture et corrigez avant de livrer :
- Une bulle ou légende couvre un visage ou un nombre clé → déplacez-la.
- Art coupé au bord de la case ou texte coupé en bas → déplacez ou réduisez.
- La queue pointe vers le mauvais personnage → changez la queue.
- Une case est à moitié vide → ajoutez un personnage, un accessoire ou un graphique plus grand.
- Les nombres correspondent à l'article. Re-rendu jusqu'à ce que ce soit propre (généralement 2 à 3 tours).
6. Livraison
- Nom du fichier
<Titre_Court>_Comic_v1.pdf; chaque révision ultérieure obtient v2, v3, ... - Envoyez avec SendUserFile. Répondez en 3 à 5 lignes courtes : le titre, la distribution, la liste des pages et une offre (une autre langue, d'autres noms de personnages, plus ou moins de pages).
- Pour une version arabe : ajoutez
dir="rtl"sur les bulles/légendes et chargez une police arabe (par ex.npm pack @fontsource/cairo) dans le CSS ; gardez les nombres et formules LTR.
Moteur : comic_engine.py (écrivez ce fichier exactement)
"""comic_engine.py — moteur réutilisable pour les explications sous forme de bande dessinée d'articles de recherche.
Coordonnées : chaque case est placée en mm sur une page A4 (210 x 297 mm). À l'intérieur d'une case,
le dessin SVG utilise U = 4 unités par mm (une case de 96 x 70 mm a viewBox 0 0 384 280).
Les personnages sont dessinés avec leurs PIEDS à (x, y) ; s = échelle (1.0 fait environ 30 mm de haut).
"""
import os, subprocess, glob
import numpy as np
U = 4
INK = "#141414"
FONT_DIR = os.environ.get("COMIC_FONT_DIR", "/tmp/comic_fonts")
# ---------------------------------------------------------------- polices
def ensure_fonts():
"""Télécharge Bangers + Comic Neue (via npm) une fois. Utilise les polices système en secours si hors ligne."""
want = ["bangers-latin-400-normal.woff2", "comic-neue-latin-400-normal.woff2", "comic-neue-latin-700-normal.woff2"]
found = {os.path.basename(p): p for p in glob.glob(f"{FONT_DIR}/**/*.woff2", recursive=True)}
if all(w in found for w in want):
return found
os.makedirs(FONT_DIR, exist_ok=True)
try:
subprocess.run(["npm", "pack", "@fontsource/bangers", "@fontsource/comic-neue"], cwd=FONT_DIR,
check=True, capture_output=True)
for t in glob.glob(f"{FONT_DIR}/*.tgz"):
d = t[:-4]
os.makedirs(d, exist_ok=True)
subprocess.run(["tar", "xzf", t, "-C", d], check=True)
except Exception as e:
print("échec du téléchargement des polices, utilisation des polices de secours :", e)
return {os.path.basename(p): p for p in glob.glob(f"{FONT_DIR}/**/*.woff2", recursive=True)}
# ---------------------------------------------------------------- personnages
def robot(x, y, s=1.0, mood="happy", arms="down", color="#29b6f6", label="1 RF", antenna=True, light="#e1f5fe"):
"""Robot héros (le système / dispositif étudié dans l'article). humeurs : happy worried surprised determined joy.
arms : down up think. antenna=True dessine une rampe de ports sur sa tête."""
mouths = {
"happy": f'<path d="M-11,-75 Q0,-64 11,-75" fill="none" stroke="{INK}" stroke-width="3" stroke-linecap="round"/>',
"worried": f'<path d="M-10,-70 Q0,-78 10,-70" fill="none" stroke="{INK}" stroke-width="3" stroke-linecap="round"/>'
f'<path d="M-18,-97 L-6,-94 M18,-97 L6,-94" stroke="{INK}" stroke-width="3" stroke-linecap="round"/>',
"surprised": f'<ellipse cx="0" cy="-72" rx="4.5" ry="6" fill="{INK}"/>',
"determined": f'<path d="M-9,-72 L9,-72" stroke="{INK}" stroke-width="3" stroke-linecap="round"/>'
f'<path d="M-18,-96 L-6,-92 M18,-96 L6,-92" stroke="{INK}" stroke-width="3.5" stroke-linecap="round"/>',
"joy": f'<path d="M-12,-76 Q0,-60 12,-76 Z" fill="#c62828" stroke="{INK}" stroke-width="2.5"/>',
}
a = {"up": "M-30,-52 L-48,-80 M30,-52 L48,-80", "think": "M-30,-52 L-44,-30 M30,-52 L22,-70"}.get(
arms, "M-30,-52 L-44,-30 M30,-52 L44,-30")
top = ""
if antenna:
dots = "".join(f'<circle cx="{ -52 + k*11.5 }" cy="-118" r="3" fill="#0277bd"/>' for k in range(10))
top = (f'<rect x="-62" y="-125" width="124" height="14" rx="7" fill="#b2ebf2" stroke="{INK}" stroke-width="3"/>{dots}'
f'<circle cx="{ -52 + 6*11.5 }" cy="-118" r="6" fill="#ffeb3b" stroke="{INK}" stroke-width="1.5"/>')
else:
top = f'<circle cx="0" cy="-122" r="7" fill="#ffeb3b" stroke="{INK}" stroke-width="2.5"/>'
return f'''<g transform="translate({x},{y}) scale({s})">
<ellipse cx="0" cy="2" rx="40" ry="6" fill="#000" opacity=".15"/>
<rect x="-21" y="-14" width="13" height="14" rx="3" fill="#455a64" stroke="{INK}" stroke-width="3"/>
<rect x="8" y="-14" width="13" height="14" rx="3" fill="#455a64" stroke="{INK}" stroke-width="3"/>
<path d="{a}" stroke="{INK}" stroke-width="11" stroke-linecap="round" fill="none"/>
<path d="{a}" stroke="{color}" stroke-width="6" stroke-linecap="round" fill="none"/>
<rect x="-32" y="-64" width="64" height="52" rx="12" fill="{color}" stroke="{INK}" stroke-width="3"/>
<rect x="-17" y="-54" width="34" height="20" rx="4" fill="#01579b" stroke="{INK}" stroke-width="2"/>
<text x="0" y="-39.5" font-size="10" fill="#fff" text-anchor="middle" font-family="Bangers">{label}</text>
<rect x="-6" y="-116" width="12" height="12" fill="#90a4ae" stroke="{INK}" stroke-width="2.5"/>
<rect x="-31" y="-106" width="62" height="44" rx="15" fill="{light}" stroke="{INK}" stroke-width="3"/>
<ellipse cx="-11" cy="-87" rx="6" ry="8" fill="{INK}"/><ellipse cx="11" cy="-87" rx="6" ry="8" fill="{INK}"/>
<circle cx="-9" cy="-90" r="2.2" fill="#fff"/><circle cx="13" cy="-90" r="2.2" fill="#fff"/>
<circle cx="-22" cy="-76" r="4" fill="#f48fb1" opacity=".7"/><circle cx="22" cy="-76" r="4" fill="#f48fb1" opacity=".7"/>
{mouths[mood]}{top}</g>'''
def scientist(x, y, s=1.0, point=False, mood="smile", flip=False, scarf="#7e57c2", headwear="hijab",
skin="#f1c27d", hair="#3e2723"):
"""Mentor / narrateur qui explique. headwear : 'hijab' ou 'hair'. humeurs : smile open serious.
point=True tient un bâton pointant vers le haut à droite (flip=True miroir)."""
mouth = {"smile": f'<path d="M-6,-82 Q0,-77 6,-82" fill="none" stroke="{INK}" stroke-width="2.2" stroke-linecap="round"/>',
"open": f'<path d="M-6,-83 Q0,-74 6,-83 Z" fill="#c62828" stroke="{INK}" stroke-width="2"/>',
"serious": f'<path d="M-5,-81 L5,-81" stroke="{INK}" stroke-width="2.2" stroke-linecap="round"/>'}[mood]
arm = (f'<path d="M18,-60 L40,-82" stroke="{INK}" stroke-width="11" stroke-linecap="round"/>'
f'<path d="M18,-60 L40,-82" stroke="#fafafa" stroke-width="6" stroke-linecap="round"/>'
f'<path d="M40,-82 L66,-108" stroke="#6d4c41" stroke-width="3.5" stroke-linecap="round"/>'
f'<circle cx="40" cy="-82" r="5" fill="{skin}" stroke="{INK}" stroke-width="2"/>') if point else \
(f'<path d="M20,-60 L28,-30" stroke="{INK}" stroke-width="11" stroke-linecap="round"/>'
f'<path d="M20,-60 L28,-30" stroke="#fafafa" stroke-width="6" stroke-linecap="round"/>')
if headwear == "hijab":
head = (f'<path d="M-25,-92 Q-30,-66 -15,-62 L15,-62 Q30,-66 25,-92 Z" fill="{scarf}" stroke="{INK}" stroke-width="3"/>'
f'<circle cx="0" cy="-94" r="25" fill="{scarf}" stroke="{INK}" stroke-width="3"/>')
else:
head = f'<path d="M-19,-86 Q-22,-116 0,-114 Q22,-116 19,-86 Q12,-104 0,-102 Q-12,-104 -19,-86 Z" fill="{hair}" stroke="{INK}" stroke-width="2.5"/>'
fl = ' scale(-1,1)' if flip else ''
body = f'''<ellipse cx="0" cy="2" rx="34" ry="6" fill="#000" opacity=".15"/>
<ellipse cx="-11" cy="-2" rx="10" ry="5" fill="#3e2723" stroke="{INK}" stroke-width="2"/>
<ellipse cx="11" cy="-2" rx="10" ry="5" fill="#3e2723" stroke="{INK}" stroke-width="2"/>
<rect x="-25" y="-20" width="50" height="80" rx="8" fill="#fafafa" stroke="{INK}" stroke-width="3"/>
<path d="M-25,-10 L25,-10" stroke="#bdbdbd" stroke-width="1.5"/>
<rect x="-18" y="-18" width="36" height="8" rx="3" fill="{scarf}" stroke="{INK}" stroke-width="1.5"/>
<g transform="translate(0,-30) scale(1.2)">{head}</g>
<ellipse cx="0" cy="-88" rx="14" ry="10" fill="{skin}" stroke="{INK}" stroke-width="2.5"/>
<circle cx="-6" cy="-92" r="2" fill="{INK}"/><circle cx="6" cy="-92" r="2" fill="{INK}"/>
{mouth}<g transform="translate(0,-30){fl}">{arm}</g></g>'''
return body