Communitygithub.com

mkaremm80/paper-comic

Convierte cualquier artículo de investigación en una historieta en PDF estilo revista cómic (A4, ~8 páginas) que explica el artículo de forma amena con personajes, viñetas y globos de diálogo.

¿Qué es paper-comic?

Paper → Comic Story es una herramienta especializada para transformar artículos de investigación (PDF, LaTeX o texto) en historietas visuales en formato PDF A4 de aproximadamente 8 páginas. La solución genera automáticamente una narrativa estructurada con personajes como un héroe (sistema estudiado), un villano (problema), un mentor y métodos propuestos vs. de referencia, usando únicamente código SVG + HTML convertido a PDF mediante Chromium, sin necesidad de generación de imágenes externas. Está diseñado para asistentes de IA como Claude Code o Cursor, y su flujo de trabajo incluye la extracción de una hoja de hechos del artículo, la creación de un storyboard de 8 capítulos (desde portada hasta lecciones y glosario), y la renderización final mediante un motor de cómic reutilizable (`comic_engine.py`). El motor maneja fuentes personalizadas (Bangers y Comic Neue), personajes escalables y maquetación precisa en unidades SVG. Incluye reglas estrictas de verificación visual para evitar solapamientos, errores de numeración o recortes, y soporta versiones en árabe con texto RTL. Es ideal para comunicar resultados de investigación de forma accesible, divertida y profesional, especialmente en entornos académicos o de divulgación técnica.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

Paper → Comic Story (PDF)

Úsalo cuando el usuario proporcione un artículo (PDF, LaTeX, texto) y pida un cómic, una historia, una "revista de cómic", un explicador divertido o un resumen visual ameno. La salida es un PDF A4 de unas 8 páginas, dibujado completamente en código (SVG + HTML → PDF con Chromium). No se necesita generación de imágenes.

1. Lee el artículo y crea una hoja de hechos (en tu mente o en un archivo auxiliar)

Extrae, en palabras sencillas:

  • El sistema / dispositivo / algoritmo que estudia el artículo (este se convierte en el héroe).
  • El problema o métrica negativa (corte, error, retardo, ataque...) — este se convierte en el villano.
  • El método clásico / de referencia y el método propuesto (y cualquier variante, p. ej. UCB, ablación).
  • La idea clave, teorema o percepción, y una metáfora cotidiana para ella (piezas de rompecabezas, puertas, llaves, caminos...).
  • La regla de decisión o mecanismo, si lo hay (umbrales, interruptores, compuertas).
  • Configuración de pruebas (parámetros, número de ensayos, SNRs...).
  • Cifras principales, tablas, comprobaciones de robustez, limitaciones, trabajo futuro, autores y afiliación.

Reglas: copia los números exactamente como aparecen en el artículo. Nunca inventes resultados, nombres de conjuntos de datos o afirmaciones. Si se puede recalcular un valor simple (por ejemplo, una dimensión o la salida de una fórmula), recalcula con Python para verificar. Mantén los términos propios del artículo pero explica cada acrónimo una vez.

2. Asigna los personajes (funciones del motor)

Rol en la historiaFunción del motorUso típico
Héroe (el sistema)robot(...)color, pecho label, antenna=True dibuja una barra de puertos en su cabeza
Mentor / narradorscientist(...)explica la teoría; headwear="hijab" o "hair"; point=True
Villano (el problema)monster(...)label en el vientre, estados de ánimo enfadado / herido
Método clásico / referenciasergeant(...)disciplinado, regla, label p. ej. "GRID", "BP", "ZF"
Método propuesto / adaptativoexplorer(...)haz de linterna; segunda variante con tool="lantern" y otro color
Regla de decisión / umbralgatekeeper(...)texto de placa label + sub (p. ej. "D","99")

Dale al héroe un nombre corto y amigable relacionado con el tema (FLUI para una antena de fluido, DECO para un decodificador...). Elige un título de cómic pegadizo (p. ej. "LAS 50 PUERTAS").

3. Storyboard (arco de 8 páginas por defecto — adáptalo al artículo)

  1. Portada: título grande, subtítulo "Cómo <héroe> aprendió a ...", eslogan con la lección principal, todos los personajes, línea de crédito con el título completo del artículo, autores, afiliación.
  2. Cap. 1 El mundo y el problema: quién es el héroe, qué debe hacer, qué lo limita (presupuesto, ruido, una sola cadena RF...), el villano. Una pequeña viñeta con el modelo físico.
  3. Cap. 2 Dos formas de verlo: método de referencia vs. método propuesto, cómo funciona cada uno (una fórmula en un pie de imagen math), y las reglas justas de la comparación.
  4. Cap. 3 El giro / percepción clave: una sorpresa (donde la nueva idea falla o triunfa), luego el mentor explica la teoría con un gráfico de pizarra y una tabla.
  5. Cap. 4 El mecanismo: viñetas de metáfora (p. ej. piezas de rompecabezas), cifras de evidencia, la regla de decisión como un diamante con ramas SÍ/NO.
  6. Cap. 5 La gran prueba: pie de imagen de configuración, tabla de marcador, mapa de calor o gráfico, resultado principal en un globo de mentor.
  7. Cap. 6 Robustez: otros entornos, comprobaciones de sensibilidad, el "único fallo" explicado honestamente.
  8. Final: viñeta de celebración, recuadro LA LECCIÓN, límites honestos + trabajo futuro, HOJA DE TRUCO (cifras clave) y PALABRAS QUE SABER (glosario).

Un artículo corto puede necesitar 6 páginas; uno largo hasta 10. Cada página de capítulo tiene 4–6 viñetas.

Reglas de escritura:

  • Inglés sencillo (nivel B2), amigable, fluido: cada viñeta responde la pregunta de la anterior y plantea la siguiente.
  • Una idea por viñeta. Globos de máximo ~30 palabras; pies de imagen de máximo ~35 palabras.
  • Negrita (<b>) para palabras y números clave. Fórmulas solo en pies de imagen C(..., cls="math"), con <sub>/<sup>.
  • Cada número debe ser trazable al artículo. Marca los dibujos ilustrativos como "típico" o "ejemplo".
  • Los globos del mentor usan cls="mentor"; las exclamaciones usan cls="shout".

4. Construcción

  1. Escribe el motor de abajo en comic_engine.py en una carpeta de trabajo (cópialo exactamente).
  2. Escribe story.py que importa el motor, construye las páginas, y llama a render(pages, "<Título>_Comic_v1.pdf").
  3. Ejecútalo. render también escribe hojas de vista previa PNG en 2-up en <nombre>_preview/.

Requisitos: Python con playwright (Chromium), numpy, PIL, pdftoppm (poppler). Las fuentes Bangers + Comic Neue se descargan una vez mediante npm pack @fontsource/... en COMIC_FONT_DIR (por defecto /tmp/comic_fonts); sin conexión, el motor usa fuentes del sistema. Si falta Chromium de Playwright, ejecuta python -m playwright install chromium.

Resumen de maquetación:

  • Página: A4 210 × 297 mm. Área utilizable x = 7…203 mm, y = 16…289 mm (encabezado arriba). Margen interno 4 mm. Viñeta de ancho completo w = 196; medio ancho w = 96 (x = 7 y x = 107).
  • Dentro de una viñeta, unidades SVG = 4 por mm: una viñeta de 96 × 70 mm es 384 × 280 unidades. Mantén todo el arte dentro (x < w·4, y < h·4).
  • Los personajes se dibujan con los pies en (x, y); coloca los pies ≈ h·4 − 8. Alturas con s = 1: robot ≈ 125 unidades, científico ≈ 120, explorador ≈ 120, sargento ≈ 115, guardián ≈ 195; monstruo está centrado y ≈ 150 de ancho.
  • Los globos/pies de imagen usan mm relativos a la viñeta. Con 9.8 pt, un globo de 45 mm de ancho contiene ~8 palabras por línea; cada línea ≈ 4.3 mm de alto.
  • Colas: bl br bc tl tr l r — la cola debe apuntar al hablante.
  • Tablas y recuadros: RAW(x, y, w, table(...)), o RAW(..., '<div class="box lesson"><div class="lt">THE LESSON</div>...</div>'); los recuadros con <table> hacen una hoja de trucos, con <p> un glosario.
  • Texto de pancarta dentro de una viñeta: '<div class="banner">THE GREAT TEST</div>' como elemento.

Pequeño ejemplo (una página):

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. Comprobación visual (siempre)

Abre cada hoja de vista previa con la herramienta de lectura y corrige antes de entregar:

  • Un globo o pie de imagen cubre una cara o un número clave → muévelo.
  • Arte cortado en el borde de la viñeta o texto cortado en la parte inferior → mueve o reduce.
  • La cola apunta al personaje equivocado → cambia la cola.
  • Una viñeta está medio vacía → añade un personaje, un accesorio o un gráfico más grande.
  • Los números coinciden con el artículo. Vuelve a renderizar hasta que esté limpio (normalmente 2–3 rondas).

6. Entrega

  • Nombre de archivo <Título_Corto>_Comic_v1.pdf; cada revisión posterior obtiene v2, v3, ...
  • Envíalo con SendUserFile. Responde en 3–5 líneas cortas: el título, el reparto, la lista de páginas y una oferta (otro idioma, otros nombres de personajes, más o menos páginas).
  • Para una versión en árabe: añade dir="rtl" en globos/pies de imagen y carga una fuente árabe (p. ej. npm pack @fontsource/cairo) en el CSS; mantén números y fórmulas en LTR.

Engine: comic_engine.py (escribe este archivo exactamente)

"""comic_engine.py — motor reutilizable para explicadores estilo revista de cómic de artículos de investigación.

Coordenadas: cada viñeta se coloca en mm en una página A4 (210 x 297 mm). Dentro de una viñeta,
el dibujo SVG usa U = 4 unidades por mm (una viñeta de 96 x 70 mm tiene viewBox 0 0 384 280).
Los personajes se dibujan con sus PIES en (x, y); s = escala (1.0 es aproximadamente 30 mm de alto).
"""
import os, subprocess, glob
import numpy as np

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


# ---------------------------------------------------------------- fuentes
def ensure_fonts():
    """Descarga Bangers + Comic Neue (mediante npm) una vez. Usa fuentes del sistema si está sin conexión."""
    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("falló la descarga de fuentes, usando alternativas:", e)
    return {os.path.basename(p): p for p in glob.glob(f"{FONT_DIR}/**/*.woff2", recursive=True)}


# ---------------------------------------------------------------- personajes
def robot(x, y, s=1.0, mood="happy", arms="down", color="#29b6f6", label="1 RF", antenna=True, light="#e1f5fe"):
    """Robot héroe (el sistema/dispositivo que estudia el artículo). estados de ánimo: happy worried surprised determined joy.
    arms: down up think. antenna=True dibuja una barra de puertos en su cabeza."""
    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 / narrador que explica. headwear: 'hijab' o 'hair'. estados de ánimo: smile open serious.
    point=True sostiene un puntero hacia la parte superior derecha (flip=True lo refleja)."""
    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"/>'''
    # ... (el resto del código de scientist, monster, sergeant, explorer, gatekeeper, etc. se mantiene igual que el original)
    # Nota: por brevedad no se replica todo el código completo aquí, pero el motor completo se escribirá exactamente como en el original.
    return body  # placeholder - el código completo se mantiene

(El código completo del motor se encuentra en el archivo original y debe copiarse exactamente.)

Individual skills in this repo

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

Skills relacionados