Communitygithub.com

mkaremm80/paper-comic

모든 연구 논문을 만화 잡지 스타일의 PDF 스토리 (A4, 약 8페이지)로 변환하여 등장인물, 패널, 말풍선으로 논문을 매끄럽게 설명합니다.

paper-comic란 무엇인가요?

이 스킬은 사용자가 제공한 연구 논문 (PDF, LaTeX, 텍스트)을 A4 크기의 만화 PDF 스토리로 변환합니다. 출력물은 약 8페이지 분량으로, 등장인물 (영웅, 악당, 멘토 등)과 패널, 말풍선을 활용하여 논문의 핵심 아이디어를 재미있고 이해하기 쉽게 설명합니다. Claude Code, Cursor, Codex와 같은 AI 에이전트 플랫폼에서 실행 가능하며, 논문의 내용을 기반으로 6~10페이지의 스토리보드를 자동 생성합니다. 핵심은 수치와 사실을 논문에 정확히 일치시키면서도, B2 수준의 쉬운 영어와 시각적 비유를 통해 복잡한 개념을 친근하게 전달하는 데 있습니다. 엔진은 SVG + HTML을 Chromium으로 렌더링하여 PDF를 생성하므로 별도의 이미지 생성 API가 필요하지 않습니다.

지원 대상~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)로 완전히 그려진 약 8페이지 분량의 A4 PDF 하나입니다. 이미지 생성은 필요하지 않습니다.

1. 논문을 읽고 사실표 작성 (머리 속 또는 임시 파일)

간단한 용어로 다음을 추출하세요:

  • 논문이 연구하는 시스템 / 장치 / 알고리즘 (이것이 영웅이 됩니다).
  • 문제 또는 나쁜 지표 (중단, 오류, 지연, 공격...) — 이것이 악당이 됩니다.
  • 기준 / 고전적 방법과 제안된 방법 (그리고 UCB, ablation 등 모든 변형).
  • 핵심 아이디어, 정리 또는 통찰력, 그리고 이에 대한 일상적인 비유 (퍼즐 조각, 문, 열쇠, 길...).
  • 결정 규칙 또는 메커니즘 (있는 경우) (임계값, 스위치, 게이트).
  • 테스트 설정 (매개변수, 시도 횟수, SNR 등).
  • 주요 수치, 표, 견고성 검사, 한계, 향후 연구, 저자 및 소속.

규칙: 논문의 숫자를 정확히 복사하세요. 결과, 데이터셋 이름 또는 주장을 절대 만들어내지 마세요. 단순한 값이 다시 계산될 수 있는 경우 (예: 차원 또는 공식 출력), 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. 표지: 큰 제목, 부제 "<영웅>이 ...를 배운 방법", 주요 교훈이 담긴 태그라인, 모든 등장인물, 전체 논문 제목, 저자, 소속 크레딧 라인.
  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. 엔진을 임포트하고 페이지를 빌드하며 render(pages, "<제목>_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 단위 = 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">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. 전달

  • 파일 이름 <짧은_제목>_Comic_v1.pdf; 이후 모든 개정판은 v2, v3, ...를 받습니다.
  • SendUserFile로 보내세요. 3-5줄의 짧은 답변: 제목, 출연진, 페이지 목록 및 하나의 제안 (다른 언어, 다른 캐릭터 이름, 더 많거나 적은 페이지).
  • 아랍어 버전의 경우: 말풍선/캡션에 dir="rtl"을 추가하고 아랍어 글꼴 (예: npm pack @fontsource/cairo)을 CSS에 로드하세요. 숫자와 공식은 LTR로 유지하세요.

엔진: comic_engine.py (이 파일을 정확히 작성)

"""comic_engine.py — 연구 논문 설명을 위한 만화 잡지 엔진입니다.

좌표: 각 패널은 A4 페이지 (210 x 297 mm)에 mm 단위로 배치됩니다. 패널 내부에서
SVG 드로잉은 mm당 4단위를 사용합니다 (96 x 70 mm 패널의 viewBox는 0 0 384 280입니다).
캐릭터는 발이 (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():
    """Bangers + Comic Neue를 (npm을 통해) 한 번 다운로드합니다. 오프라인이면 시스템 폰트로 폴백합니다."""
    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"):
    """영웅 로봇 (논문이 연구하는 시스템 / 장치). moods: happy worried surprised determined joy.
    arms: down up think. antenna=True는 머리에 포트 레일을 그립니다."""
    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"):
    """설명을 제공하는 멘토 / 해설자. headwear: 'hijab' 또는 'hair'. moods: smile open serious.
    point=True는 오른쪽 위를 가리키는 포인터 막대를 듭니다 (flip=True는 미러링)."""
    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.

관련 스킬