Communitygithub.com

mkaremm80/paper-comic

Transformez n'importe quel article de recherche en une bande dessinée style magazine comique au format PDF (A4, ~8 pages) qui explique l'article de manière fluide avec des personnages, des cases et des bulles de dialogue.

Qu'est-ce que paper-comic ?

Cette compétence permet de transformer un article de recherche scientifique (PDF, LaTeX ou texte brut) en une bande dessinée ludique et visuelle au format PDF A4, d'environ 8 pages. Elle est utile pour les chercheurs, les enseignants ou toute personne souhaitant vulgariser un contenu technique complexe de manière accessible et engageante, sans génération d'images. La compétence prend en charge les plateformes d'agents comme Claude Code, Cursor ou Codex, et repose sur un moteur qui génère des pages à l'aide de SVG, HTML et Chromium (Playwright). Le processus comprend la lecture de l'article, la création d'une fiche de faits (héros, problème, méthodes proposées et de base), le casting de personnages (robot, scientifique, monstre, etc.), la construction d'un storyboard en 8 chapitres, le rendu via un fichier Python (comic_engine.py + story.py), et une vérification visuelle interactive. Les règles strictes garantissent la fidélité aux données de l'article (copie exacte des chiffres, pas d'invention) et une rédaction simple en anglais niveau B2, avec bulles de ~30 mots maximum.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/mkaremm80/-my-claude-skills/tree/HEAD/skills/paper-comic

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

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'histoireFonction du moteurUtilisation typique
Héros (le système)robot(...)couleur, label sur la poitrine, antenna=True dessine une rampe de ports sur sa tête
Mentor / narrateurscientist(...)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 / classiquesergeant(...)discipliné, règle, label par ex. "GRID", "BP", "ZF"
Méthode proposée / adaptativeexplorer(...)faisceau de lampe torche ; deuxième variante avec tool="lantern" et une autre couleur
Règle de décision / seuilgatekeeper(...)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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. Ch.6 Robustesse : autres paramètres, vérifications de sensibilité, le "seul échec" expliqué honnêtement.
  8. 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égendes C(..., 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 utilisent cls="shout".

4. Construction

  1. Écrivez le moteur ci-dessous dans comic_engine.py dans un dossier de travail (copiez-le exactement).
  2. Écrivez story.py qui importe le moteur, construit les pages et appelle render(pages, "<Title>_Comic_v1.pdf").
  3. 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(...)), ou RAW(..., '<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

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

Skills associés