Communitygithub.com

mkaremm80/paper-comic

あらゆる研究論文を、キャラクターやパネル、吹き出しを用いた漫画マガジンスタイルのPDFストーリー(A4、約8ページ)に変換します。

paper-comic とは?

このスキルは、研究論文(PDF、LaTeX、テキスト形式)を読み込み、その内容をA4サイズ約8ページの漫画形式のPDFに変換するものです。論文内のシステム、問題点、提案手法、重要な洞察など、核となる要素を抽出し、それらをヒーロー、悪役、メンターといったキャラクターに見立ててストーリーを構築します。各パネルはSVGとHTMLでコード生成され、PlaywrightのChromiumを使ってPDFにレンダリングされます。画像生成は不要で、すべてのイラストはプログラムによって描画されます。このスキルは、研究の要点を視覚的で楽しく、記憶に残りやすい形で伝えるために設計されており、論文の内容を簡潔に理解したい開発者や研究者に特に有用です。対応プラットフォームはClaude Codeを前提としており、Python(Playwright、NumPy、Pillow)とフォント(Bangers、Comic Neue)を使用します。

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

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

論文 → コミックストーリー(PDF)

ユーザーが論文(PDF、LaTeX、テキスト)を提供し、コミック、ストーリー、「コミックマガジン」、漫画風の解説、楽しいビジュアル要約を依頼した場合に使用します。出力は、コード(SVG + HTML → Chromium PDF)で完全に描画された、A4サイズ約8ページのPDF1つです。画像生成は必要ありません。

1. 論文を読み、ファクトシートを作成する(頭の中で、またはスクラッチファイルに)

以下の情報を平易な言葉で抽出します。

  • 論文が研究するシステム/デバイス/アルゴリズム(これが主人公になります)。
  • 問題または悪い指標(アウテージ、エラー、遅延、攻撃...)— これが悪役になります。
  • ベースライン/従来手法と提案手法(および任意のバリアント、例:UCB、アブレーション)。
  • 主要なアイデア、定理、洞察、およびそれに対する日常的なメタファー(パズルのピース、ドア、鍵、道...)。
  • 決定ルールまたはメカニズム(もしあれば)(しきい値、スイッチ、ゲート)。
  • テスト設定(パラメータ、試行回数、SNR...)。
  • 主要な数値、テーブル、ロバスト性チェック、限界、将来の研究、著者と所属。

ルール:論文の数値をそのままコピーします。結果、データセット名、主張を決して捏造しないでください。単純な値が再計算可能な場合(例:次元や数式の出力)、Pythonで再計算して確認します。論文の用語はそのまま使用しますが、各略語は一度説明します。

2. キャラクターをキャスティングする(エンジン関数)

ストーリーでの役割エンジン関数典型的な使用法
主人公(システム)robot(...)色、胸の label、antenna=True で頭にポートのレールを描画
メンター/ナレーターscientist(...)理論を説明;headwear="hijab" または "hair";point=True
悪役(問題)monster(...)お腹に label、ムード angry / hurt
ベースライン/従来手法sergeant(...)規律正しい、定規、label 例 "GRID"、"BP"、"ZF"
提案/適応手法explorer(...)懐中電灯の光;第2バリアントは tool="lantern" と別の色
決定ルール/しきい値gatekeeper(...)銘板テキスト label + sub(例 "D"、"99")

主人公には、トピックに関連した短く親しみやすい名前を付けます(流体アンテナならFLUI、デコーダならDECO...)。キャッチーなコミックタイトルを選びます(例:「THE 50 DOORS」)。

3. ストーリーボード(デフォルト8ページのアーク — 論文に合わせて調整)

  1. カバー:大きなタイトル、サブタイトル「<主人公>が...を学んだ方法」、主な教訓を伝えるタグライン、全キャラクター、論文の完全なタイトル、著者、所属を含むクレジット行。
  2. 第1章 世界と問題:主人公が誰か、何をすべきか、何がそれを制限するか(予算、ノイズ、1つのRFチェーン...)、悪役。物理モデルを示す小さなボード1つ。
  3. 第2章 二つの見方:ベースラインと提案手法、それぞれの仕組み(math キャプション内の数式1つ)、比較の公平なルール。
  4. 第3章 ひねり / 重要な洞察:驚き(新しいアイデアが失敗または成功する場面)、その後メンターが黒板の図とテーブルで理論を説明。
  5. 第4章 メカニズム:メタファーパネル(例:パズルのピース)、証拠となる数値、YES/NOの分岐を持つダイアモンド型の決定ルール。
  6. 第5章 大テスト:設定のキャプション、スコアボードテーブル、ヒートマップまたはチャート、メンターの吹き出しでの主要結果。
  7. 第6章 ロバスト性:その他の設定、感度チェック、「唯一の見逃し」を正直に説明。
  8. フィナーレ:祝賀パネル、「THE LESSON」ボックス、正直な限界 + 将来の研究、「CHEAT SHEET」(主要数値)、「WORDS TO KNOW」(用語集)。

短い論文は6ページで十分かもしれません。長いものは最大10ページまで。各章のページには4〜6個のパネルがあります。

執筆ルール:

  • 簡単な英語(B2レベル)、親しみやすく、スムーズ:各パネルは前のパネルの質問に答え、次の質問を提起します。
  • 1パネル1アイデア。吹き出しは最大約30語。キャプションは最大約35語。
  • キーワードと数値は太字(<b>)に。数式は C(..., cls="math") キャプション内のみ、<sub>/<sup> を使用。
  • すべての数値は論文にトレース可能に。説明用のイラストは「典型的な」または「例」とマーク。
  • メンターの吹き出しは cls="mentor" を使用。感嘆文は cls="shout" を使用。

4. 構築

  1. 以下のエンジンをワーキングフォルダに comic_engine.py として書き込みます(正確にコピー)。
  2. エンジンをインポートし、ページを構築し、render(pages, "<Title>_Comic_v1.pdf") を呼び出す story.py を書きます。
  3. 実行します。render は2-upのPNGプレビューシートも <name>_preview/ に書き込みます。

要件:Python(playwright(Chromium)、numpy、PIL、pdftoppm(poppler))。フォントBangers + Comic Neueは、npm pack @fontsource/... によって一度 COMIC_FONT_DIR(デフォルト /tmp/comic_fonts)に取得されます。オフラインの場合、エンジンはシステムフォントにフォールバックします。PlaywrightのChromiumがない場合は、python -m playwright install chromium を実行します。

レイアウトのチートシート:

  • ページ:A4 210 × 297 mm。使用可能領域 x = 7…203 mm、y = 16…289 mm(ヘッダーは上)。ガター 4 mm。全幅パネル w = 196。半幅 w = 96(x = 7 および x = 107)。
  • パネル内では、SVG単位は1mmあたり4:96 × 70 mmのパネルは384 × 280ユニット。すべてのアートを内部に収めます(x < w·4、y < h·4)。
  • キャラクターは (x, y) に足を置いて描画されます。足 ≈ h·4 − 8。s = 1での高さ:robot ≈ 125ユニット、scientist ≈ 120、explorer ≈ 120、sergeant ≈ 115、gatekeeper ≈ 195。monsterは中央揃えで幅約150。
  • 吹き出し/キャプションはパネルを基準にmmを使用。9.8ptで幅45mmの吹き出しは1行あたり約8語を保持。各行の高さは約4.3mm。
  • テール:bl br bc tl tr l r — テールは話し手を指す必要があります。
  • テーブルとボックス:RAW(x, y, w, table(...))、または RAW(..., '<div class="box lesson"><div class="lt">THE LESSON</div>...</div>')。<table> のボックスはチートシート、<p> のボックスは用語集になります。
  • パネル内のバナーテキスト:アイテムとして '<div class="banner">THE GREAT TEST</div>'。

小さな例(1ページ):

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, "In a city full of radio waves lives <b>FLUI</b>..."),
     B(100, 44, 92, "I have <b>50 ports</b> but only <b>one</b> radio chain!", "tl")]))
p.append(panel(7, 90, 96, 70, scientist(78, 270, 1.4, point=True) + monster(290, 110, 1.05),
    [B(3, 3, 50, "Pick a weak port and the <b>Outage Monster</b> wins.", "br", "mentor"),
     B(42, 50, 50, "Below the threshold? You are MINE!", "tr", "shout")]))
pages = [cover("THE 50 DOORS", "How a one-ear antenna learned <i>when</i> to look smart", art,
               "Look where it matters...<br>but only when it matters!", credit_html),
         page(2, "Chapter 1 · One Ear, Fifty Doors", p)]
render(pages, "The_50_Doors_Comic_v1.pdf")

5. ビジュアルチェック(常に行う)

すべてのプレビューシートをReadツールで開き、配信前に修正します:

  • 吹き出しやキャプションが顔や主要な数値を覆っている → 移動します。
  • アートがパネルの端で切れている、またはテキストが下部で切れている → 移動または縮小します。
  • テールが間違ったキャラクターを指している → テールを変更します。
  • パネルが半分空っぽ → キャラクター、小道具、またはより大きなチャートを追加します。
  • 数値が論文と一致していること。

きれいになるまで再レンダリングします(通常2〜3回)。

6. 配信

  • ファイル名 <Short_Title>_Comic_v1.pdf。以降のリビジョンは v2、v3、... とします。
  • SendUserFileで送信します。3〜5行の短い返信:タイトル、キャスト、ページリスト、および1つのオファー(別の言語、他のキャラクター名、ページ数の増減)。
  • アラビア語版の場合:吹き出し/キャプションに dir="rtl" を追加し、アラビア語フォント(例:npm pack @fontsource/cairo)をCSSにロードします。数値と数式はLTRのままにします。

エンジン:comic_engine.py(このファイルを正確に書き込む)

"""comic_engine.py — reusable engine for comic-magazine explainers of research papers.

Coordinates: each panel is placed in mm on an A4 page (210 x 297 mm). Inside a panel,
SVG drawing uses U = 4 units per mm (a 96 x 70 mm panel has viewBox 0 0 384 280).
Characters are drawn with their FEET at (x, y); s = scale (1.0 is about 30 mm tall).
"""
import os, subprocess, glob
import numpy as np

U = 4
INK = "#141414"
FONT_DIR = os.environ.get("COMIC_FONT_DIR", "/tmp/comic_fonts")


# ---------------------------------------------------------------- fonts
def ensure_fonts():
    """Download Bangers + Comic Neue (via npm) once. Falls back to system fonts if offline."""
    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("font download failed, using fallbacks:", e)
    return {os.path.basename(p): p for p in glob.glob(f"{FONT_DIR}/**/*.woff2", recursive=True)}


# ---------------------------------------------------------------- characters
def robot(x, y, s=1.0, mood="happy", arms="down", color="#29b6f6", label="1 RF", antenna=True, light="#e1f5fe"):
    """Hero robot (the system / device the paper studies). moods: happy worried surprised determined joy.
    arms: down up think. antenna=True draws a rail of ports on its head."""
    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 / narrator who explains. headwear: 'hijab' or 'hair'. moods: smile open serious.
    point=True holds a pointer stick toward the upper right (flip=True mirrors)."""
    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

**[snip - 残りは元のSKILL.mdと同じPythonコードが続きます]**

Individual skills in this repo

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

関連スキル