Communitygithub.com

mkaremm80/paper-comic

将任意研究论文转化为漫画杂志风格的PDF故事(A4,约8页),通过角色、分镜和对话气泡流畅解释论文内容。

paper-comic 是什么?

这个技能能将一篇研究论文(PDF、LaTeX 或纯文本)自动生成为一个约8页的漫画杂志风格 PDF 故事。它完全通过代码(SVG + HTML 渲染为 Chromium PDF)绘制,无需借助外部图像生成模型。该技能首先从论文中抽取关键要素,包括研究对象(英雄)、问题(反派)、基准方法和提议方法等,并为其赋予漫画角色(机器人、科学家、怪物等)。随后按照默认的7–10页分镜模板(封面、问题、两种方法、关键洞察、机制、测试、鲁棒性、结局)组织内容,每页包含4–6个面板。写作遵循简单英文(B2级别),每个气泡不超过30词,所有数字均可追溯至原文。最终输出为 A4 格式的 PDF,并附带用于预览的 2-up PNG 缩略图。支持多种平台,要求安装 Python(Playwright、NumPy、Pillow)和 Poppler(pdftoppm)。

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

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

论文 → 漫画故事 (PDF)

当用户提供一篇论文(PDF、LaTeX、文本)并请求将其制作成漫画、故事、"comix 杂志"、卡通解释或有趣的视觉摘要时使用此技能。输出为一个 A4 PDF,约 8 页,完全通过代码(SVG + HTML → Chromium PDF)绘制。无需图像生成。

1. 阅读论文并制作事实清单(在脑内或草稿文件中)

用简单的语言提取:

  • 论文研究的系统/设备/算法(这将成为英雄)。
  • 问题或糟糕的指标(中断、误差、延迟、攻击……)——这将成为反派。
  • 基线/经典方法和提出的方法(以及任何变体,例如 UCB、消融实验)。
  • 核心思想、定理或洞见,以及一个日常比喻(拼图块、门、钥匙、道路……)。
  • 决策规则或机制,如果有的话(阈值、开关、门)。
  • 测试设置(参数、试验次数、信噪比……)。
  • 关键数字、表格、鲁棒性检查、局限性、未来工作、作者和所属单位。

规则:严格复制论文中的数字。切勿编造结果、数据集名称或主张。如果某个简单值可以重新计算(例如维度或公式输出),使用 Python 重新计算以验证。保留论文自己的术语,但每个缩略词需解释一次。

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

故事中的角色引擎函数典型用途
英雄(系统)robot(...)颜色,胸前 label,antenna=True 在其头部绘制一串端口导轨
导师/叙述者scientist(...)解释理论;headwear="hijab" 或 "hair";point=True
反派(问题)monster(...)腹部 label,情绪为 angry / hurt
基线/经典方法sergeant(...)纪律严明,持尺子,label 例如 "GRID"、"BP"、"ZF"
提出/自适应方法explorer(...)手电筒光束;第二个变体使用 tool="lantern" 和另一种颜色
决策规则/阈值gatekeeper(...)标牌文本 label + sub(例如 "D","99")

给英雄一个与主题相关的简短友好名称(流体天线用 FLUI,解码器用 DECO……)。选择一个朗朗上口的漫画标题(例如 "THE 50 DOORS")。

3. 故事板(默认 8 页弧线——根据论文调整)

  1. 封面:大标题,副标题 "How learned to ...",带有主要教训的标语,所有角色,致谢行包含完整论文标题、作者和所属单位。
  2. 第 1 章 世界和问题:英雄是谁,它必须做什么,它的限制是什么(预算、噪声、一条射频链路……),反派。一个展示物理模型的小画板。
  3. 第 2 章 两种方式:基线方法与提出的方法,每种方法如何工作(在 math 标题中放置一个公式),以及比较的公平规则。
  4. 第 3 章 转折/关键洞见:一个惊喜(新想法失败或成功的地方),然后导师用黑板图表和表格解释理论。
  5. 第 4 章 机制:比喻面板(例如拼图块)、证据数字、决策规则(带有 YES/NO 分支的菱形)。
  6. 第 5 章 大测试:设置标题、记分牌表格、热图或图表、导师气泡中的关键结果。
  7. 第 6 章 鲁棒性:其他设置、灵敏度检查、诚实地解释 "only miss"。
  8. 结局:庆祝面板、"THE LESSON" 框、诚实的局限性 + 未来工作、"CHEAT SHEET"(关键数字)和 "WORDS TO KNOW"(术语表)。

短论文可能需要 6 页;长论文最多 10 页。每个章节页有 4–6 个面板。

写作规则:

  • 简单的英语(B2 级别),友好,流畅:每个面板回答上一个面板的问题并提出下一个问题。
  • 一个面板一个想法。气泡最多约 30 词;标题最多约 35 词。
  • 使用 <b> 加粗关键词和数字。公式仅放在 C(..., cls="math") 标题中,使用 <sub>/<sup>。
  • 每个数字都可追溯到论文。将说明性绘图标记为 "typical" 或 "example"。
  • 导师气泡使用 cls="mentor";感叹使用 cls="shout"。

4. 构建

  1. 将下面的引擎写入工作目录的 comic_engine.py(精确复制)。
  2. 编写 story.py,导入引擎,构建页面,调用 render(pages, "<Title>_Comic_v1.pdf")。
  3. 运行它。render 还会在 <name>_preview/ 中写入 2-up 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 单位 = 每毫米 4 个:一个 96 × 70 mm 的面板是 384 × 280 单位。将所有图形保持在内部(x < w·4,y < h·4)。
  • 角色以其脚部在 (x, y) 绘制;将脚部放在 y ≈ h·4 − 8。s = 1 时的高度:机器人 ≈ 125 单位,科学家 ≈ 120,探索者 ≈ 120,中士 ≈ 115,看门人 ≈ 195;怪物居中,宽度 ≈ 150。
  • 气泡/标题使用相对于面板的毫米单位。在 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">THE LESSON</div>...</div>');带 <table> 的框制作速查表,带 <p> 的制作术语表。
  • 面板内的横幅文本:将 '<div class="banner">THE GREAT TEST</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, "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. 视觉检查(始终执行)

使用阅读工具打开每个预览表,在交付前修复:

  • 气泡或标题遮挡了面部或关键数字 → 将其移动。
  • 图形被面板边缘裁剪或文本被底部裁剪 → 移动或缩小。
  • 尾巴指向错误的角色 → 更改尾巴。
  • 面板半空 → 添加角色、道具或更大的图表。
  • 数字与论文匹配。 重新渲染直至干净(通常 2–3 轮)。

6. 交付

  • 文件名 <Short_Title>_Comic_v1.pdf;每个后续修订版为 v2、v3……
  • 使用 SendUserFile 发送。回复 3–5 行简短内容:标题、角色阵容、页面列表以及一个提议(另一种语言、其他角色名称、更多或更少的页面)。
  • 对于阿拉伯语版本:在气泡/标题上添加 dir="rtl" 并加载阿拉伯语字体(例如 npm pack @fontsource/cairo)到 CSS 中;保持数字和公式从左到右。

引擎: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"`

(由于篇幅限制,引擎的完整代码已省略。在实际文件中,请复制原始仓库中的完整 comic_engine.py 内容。)

Individual skills in this repo

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

相关技能