Communitygithub.com

mkaremm80/paper-comic

將任何研究論文轉化為漫畫雜誌風格的 PDF 故事(A4,約8頁),透過角色、分鏡和對話框流暢地解說論文內容。

paper-comic 是什麼?

Paper → Comic Story (PDF) 技巧是一個專業工具,專為 AI 代理(如 Claude Code、Cursor、Codex)設計,能將學術論文(PDF、LaTeX、純文字)自動轉換為 A4 大小的漫畫雜誌風格 PDF。它透過一個內建的引擎(comic_engine.py)生成約 8 頁的連環漫畫,完全使用程式碼(SVG + HTML)繪製,再透過 Chromium 輸出為 PDF,完全不需依賴外部圖片生成。這個技巧的獨特價值在於能將複雜的學術概念故事化:它會先提取論文的事實摘要,然後將系統/裝置化身為英雄、問題化身為反派、方法論化身為角色,並按照預設的 8 章結構(封面、問題、方法比較、關鍵見解、機制、測試、穩健性、結局與總結)來安排情節。每個對話框和圖表都緊密對應論文中的原始數據,確保科學準確性。支援多種平台(任何能執行 Python + Playwright 的環境),並內建視覺檢查與阿拉伯語版本(RTL 方向)功能,適合需要快速、生動地傳達研究核心概念的開發者、研究人員或教育者使用。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/mkaremm80/-my-claude-skills/tree/HEAD/skills/paper-comic

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

Paper → Comic Story (PDF)

當使用者提供一篇論文(PDF、LaTeX、文字)並要求將其轉換為漫畫、故事、漫畫雜誌、卡通解說或有趣的視覺摘要時,請使用此技巧。輸出是一個 A4 PDF,約 8 頁,完全透過程式碼(SVG + HTML → Chromium PDF)繪製。無需圖片生成。

1. 閱讀論文並建立事實摘要(在腦中或草稿檔案中)

以簡單的詞彙提取:

  • 論文研究的系統 / 設備 / 演算法(這將成為英雄)。
  • 問題或不良指標(中斷、錯誤、延遲、攻擊...)——這將成為反派。
  • 基線 / 經典方法和提出的方法(以及任何變體,例如 UCB、消融實驗)。
  • 關鍵想法、定理或見解,以及一個日常生活比喻(拼圖碎片、門、鑰匙、道路...)。
  • 決策規則或機制(閾值、開關、閘門)。
  • 測試設定(參數、試驗次數、SNR...)。
  • 標題數字、表格、穩健性檢查、限制、未來工作、作者和所屬機構。

規則:完全照抄論文中的數字。絕不虛構結果、資料集名稱或主張。如果一個簡單值可以重新計算(例如維度或公式輸出),請使用 Python 重新計算以確認。保留論文本身的術語,但每個縮寫需解釋一次。

2. 角色分配(引擎函式)

故事中的角色引擎函式典型用途
英雄(系統)robot(...)顏色、胸部 label、antenna=True 在頭頂畫出一排連接埠
導師 / 旁白scientist(...)解釋理論;headwear="hijab" 或 "hair";point=True
反派(問題)monster(...)標籤在肚子上,表情 angry / hurt
基線 / 經典方法sergeant(...)嚴謹、帶有尺、標籤例如 "GRID"、"BP"、"ZF"
提出的 / 自適應方法explorer(...)手電筒光束;第二變體使用 tool="lantern" 和另一種顏色
決策規則 / 閾值gatekeeper(...)牌匾文字 label + sub(例如 "D"、"99")

為英雄取一個與主題相關的簡短友好名稱(FLUI 代表流體天線,DECO 代表解碼器...)。選擇一個朗朗上口的漫畫標題(例如 "THE 50 DOORS")。

3. 分鏡腳本(預設 8 頁故事弧——根據論文調整)

  1. 封面:大標題、副標題 "How learned to ..."、帶有主要教訓的標語、所有角色、包含完整論文標題、作者、所屬機構的致謝行。
  2. 第 1 章 世界與問題:英雄是誰、它必須做什麼、限制它的因素(預算、噪音、一個 RF 鏈...)、反派。一個帶有物理模型的小圖板。
  3. 第 2 章 兩種觀點:基線方法與提出的方法,每種方法如何運作(在 math 標題中放一個公式),以及比較的公平規則。
  4. 第 3 章 轉折 / 關鍵見解:一個驚喜(新想法失敗或成功的地方),然後導師用黑板圖表和一個表格解釋理論。
  5. 第 4 章 機制:比喻面板(例如拼圖碎片)、證據數字、帶有是/否分支的菱形決策規則。
  6. 第 5 章 大考驗:設定說明、記分板表格、熱力圖或圖表、導師對話框中的標題結果。
  7. 第 6 章 穩健性:其他設定、敏感度檢查、誠實解釋 "唯一失誤"。
  8. 結局:慶祝面板、教訓框、誠實的限制 + 未來工作、秘笈(關鍵數字)和詞彙表。

較短的論文可能需要 6 頁;較長的可能需要 10 頁。每個章節頁面有 4-6 個面板。

寫作規則:

  • 簡單的英語(B2 等級),友好流暢:每個面板回答上一個面板的問題,並提出下一個問題。
  • 每個面板一個想法。對話框最多約 30 個詞;標題最多約 35 個詞。
  • 加粗(<b>)關鍵詞和數字。公式僅在 C(..., cls="math") 標題中使用,搭配 <sub>/<sup>。
  • 每個數字都能在論文中追溯到。標記插圖為 "典型" 或 "範例"。
  • 導師對話框使用 cls="mentor";驚嘆號使用 cls="shout"。

4. 建置

  1. 將以下引擎寫入工作資料夾中的 comic_engine.py(完全複製)。
  2. 撰寫 story.py,匯入引擎、建置頁面,並呼叫 render(pages, "<Title>_Comic_v1.pdf")。
  3. 執行它。render 也會在 <name>_preview/ 中寫入 2 合 1 PNG 預覽頁。

需求: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 單位 = 每 mm 4:一個 96 × 70 mm 的面板是 384 × 280 單位。將所有圖形保持在內部(x < w·4,y < h·4)。
  • 角色以腳部在 (x, y) 繪製;將腳部放在 ≈ h·4 − 8。縮放比例 s = 1:機器人大約 125 單位,科學家約 120,探險家約 120,軍士約 115,守門人約 195;怪物居中且約 150 寬。
  • 對話框/標題使用相對於面板的 mm。在 9.8 pt 下,一個 45 mm 寬的對話框每行約可容納 8 個詞;每行約 4.3 mm 高。
  • 尾巴:bl br bc tl tr l r — 尾巴必須指向說話者。
  • 表格和方框:RAW(x, y, w, table(...)) 或 RAW(..., '<div class="box lesson"><div class="lt">教訓</div>...</div>');帶有 <table> 的方框製作秘笈,帶有 <p> 的方框製作詞彙表。
  • 面板內的橫幅文字:'<div class="banner">大考驗</div>' 作為一個項目。

小型範例(一頁):

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, "在一個充滿無線電波的城市裡住著 <b>FLUI</b>..."),
     B(100, 44, 92, "我有 <b>50 個連接埠</b>,但只有 <b>一個</b> 無線電鏈!", "tl")]))
p.append(panel(7, 90, 96, 70, scientist(78, 270, 1.4, point=True) + monster(290, 110, 1.05),
    [B(3, 3, 50, "選錯連接埠,<b>中斷怪獸</b>就會贏。", "br", "mentor"),
     B(42, 50, 50, "低於閾值?你就是我的了!", "tr", "shout")]))
pages = [cover("THE 50 DOORS", "一隻單耳天線如何學會聰明地觀看", art,
               "看對地方...<br>但只在需要時!", credit_html),
         page(2, "第 1 章 · 一隻耳朵,五十道門", p)]
render(pages, "The_50_Doors_Comic_v1.pdf")

5. 視覺檢查(務必執行)

使用閱讀工具打開每個預覽頁,並在交付前修正:

  • 對話框或標題遮蓋了臉部或關鍵數字 → 移動它。
  • 圖形在面板邊緣被切斷或文字在底部被切斷 → 移動或縮小。
  • 尾巴指向錯誤的角色 → 更改尾巴。
  • 面板有一半是空的 → 添加角色、道具或更大的圖表。
  • 數字與論文相符。

重新渲染直到乾淨(通常 2–3 輪)。

6. 交付

  • 檔案名稱 <Short_Title>_Comic_v1.pdf;每個後續版本命名為 v2、v3...
  • 使用 SendUserFile 傳送。用 3–5 行簡短回覆:標題、角色陣容、頁面列表,以及一個提議(其他語言、其他角色名稱、增加或減少頁數)。
  • 對於阿拉伯語版本:在對話框/標題上添加 dir="rtl",並在 CSS 中載入阿拉伯字型(例如 npm pack @fontsource/cairo);保持數字和公式為 LTR。

引擎:comic_engine.py(完全照抄這個檔案)

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

座標:每個面板以 mm 為單位放置在 A4 頁面上(210 x 297 mm)。在面板內部,
SVG 繪圖使用 U = 4 units per mm(一個 96 x 70 mm 的面板具有 viewBox 0 0 384 280)。
角色以其 FOOT 在 (x, y) 處繪製;s = 縮放比例(1.0 大約 30 mm 高)。
"""
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("字型下載失敗,使用備用方案:", 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...'''

Individual skills in this repo

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

相關技能