논문 → 게임형 학습 코스 발행 (v2)
논문 아카이브 → Gemini로 게임형 코스 만들고 웹에 올림.
논문 → 게임 코스 발행 (v2)
Gemini 2.5-pro로 논문 → 5~7개 레슨 코스 변환. 각 레슨: 이모지, 짧은 제목, 핵심 비유, matplotlib 시각화, 4지선다 퀴즈 포함. 중학생 수준 한국어, 일상 비유. HuggingFace 모델 있으면 inference 데모도 추가됨.
아키텍처 (v2: 두 머신 씀)
브라우저 ─HTTPS→ son-wtr (웹/cert/dns)
│
├ /thesis/ → 정적 index.html
└ /thesis/<id>/ → proxy_pass son-pro6000:<port>
↓
son-pro6000 (GPU)
systemd: papergen-[id].service
marimo run --base-url /thesis/[id]
port 27000~27999
왜 두 머신 쓰냐면: GPU는 son-pro6000에만 있고, 외부 IP/DNS는 son-wtr에 묶여 있어서.
son-pro6000의 마리모 프로세스가 GPU에서 데모 코드 실행, son-wtr의 nginx가 HTTPS 종료 + 리버스 프록시.
위치
son-pro6000 (GPU 서버, papergen 메인)
- 프로젝트: /home/son/prj/thesis
- venv: .venv (Python 3.10.19, torch+CUDA+transformers)
- 출력 노트북: output/paper_[id].py
- systemd unit: /etc/systemd/system/papergen-[id].service
son-wtr (웹 서버, 프록시 + cert)
- nginx site: /etc/nginx/sites-enabled/learn.ericfromkorea.com
- 논문별 location: /etc/nginx/snippets/learn-papers/[id].conf (SSH로 원격 작성)
- 인덱스 HTML: /var/www/learn.ericfromkorea.com/thesis/index.html
SSH 백채널: son-pro6000은 son-wtr로 키 인증 SSH 가능.
publish.py가 SSH로 nginx snippet 쓰고 reload, index 갱신 다 함.
UFW (son-pro6000): 포트 27000~27999 LAN (192.168.1.0/24)만 허용. 외부 차단.
논문 추가 (반복 작업)
ssh 192.168.1.116
cd ~/prj/thesis
.venv/bin/python main.py [arXiv ID] --publish
자동으로 아래 작업 진행:
1. arxiv 메타 + PDF 다운로드
2. PyMuPDF 텍스트 추출
3. Gemini 2.5-pro 호출 (Course schema, 5~7 레슨)
4. marimo .py 노트북 생성 (각 레슨에 viz + quiz 셀)
5. systemd unit 작성 (로컬), nginx snippet + index (son-wtr 원격)
바로 https://learn.ericfromkorea.com/thesis/[arXiv ID]/ 접근 가능.
발행 취소
ssh 192.168.1.116
cd ~/prj/thesis
.venv/bin/python -c "from papergen.publish import unpublish; unpublish('[arXiv ID]')"
디버깅
- 셀 실행 에러:
ssh 192.168.1.116 sudo journalctl -u papergen-[id].service -n 100 - HTTPS 상태:
curl -sI https://learn.ericfromkorea.com/thesis/[id]/ - WebSocket 직접 검증 (셀이 정말 실행되는지):
# son-pro6000에서 .venv/bin/python으로
import asyncio, json
from websockets.client import connect
async def t():
async with connect("ws://127.0.0.1:27000/thesis/[id]/ws?session_id=x") as ws:
for _ in range(50):
print(json.loads(await asyncio.wait_for(ws.recv(), 5))[:200])
asyncio.run(t())
- 셀 실행 일괄 테스트:
marimo export html output/paper_[id].py -o /tmp/x.html→ 모든 셀 서버 사이드 실행. stderr에 에러 표시. exit 0 이면 깨끗.
함정 / 배운 점
- Chromium snap + AppArmor: 헤드리스 스크린샷 안 됨. WS handshake 직접 검증으로 퉁침.
- LLM이 import 빼먹음:
plt쓰는데import matplotlib.pyplot as plt없으면 NameError. 해결:_run함수에import torch, numpy as np, matplotlib.pyplot as plt자동 삽입. - LLM이 dropdown 옵션에 따옴표 포함:
["'It'", "'was'"]같이 보낼 때 있음. sanitize 고려. - multi-line 문자열 금지: prompt에 명시. cell-body indenter가 내용 망가뜨림.
- 포트 범위 27000~27999:
state.json이 정함. UFW LAN 허용 룰 한 번만 추가하면 끝. - 모든 모듈 150줄 미만 룰.
publish.py,prompts.py가 제일 김.
여기서 배울 것
- 분산 아키텍처에서 GPU 서버와 웹 서버 역할 분리 중요함.
- systemd로 서비스 자동 관리, nginx로 리버스 프록시 설정.
- LLM 코드 생성 시 import 누락, 따옴표 문제 같은 함정 미리 대비해야 함.
- SSH 백채널로 원격 서버 설정 자동화 가능.
원본 파일 보기 (.claude/skills/tn-thesis-publish/SKILL.md)
---
name: 논문 인터랙티브 학습 코스 발행 (papergen v2)
description: Use when the user asks to turn an ML/DL paper (arXiv URL, arXiv ID, or local PDF) into a Duolingo/Elevate-style game-based interactive learning course at learn.ericfromkorea.com/thesis/<id>/. Wraps fetch -> Gemini course generation (lessons + quizzes + viz) -> marimo notebook -> systemd service on GPU server -> nginx proxy on web server.
version: 2.0.0
---
# 논문 → learn.ericfromkorea.com/thesis/<id>/ 게임형 학습 코스
Gemini 2.5-pro로 논문을 5~7개 레슨으로 구성된 게임형 코스로 변환:
- 각 레슨: 이모지 + 짧은 제목 + 핵심 비유 + matplotlib 시각화 + 4지선다 퀴즈
- 중학생 수준 한국어 + 일상 비유
- HuggingFace 모델 있으면 실제 inference 재현 데모 추가
## 아키텍처 (v2: 두 머신 분산)
```
브라우저 ─HTTPS→ son-wtr (192.168.1.121, web/cert/dns)
│
├ /thesis/ → 정적 index.html
└ /thesis/<id>/ → proxy_pass 192.168.1.116:<port>
↓
son-pro6000 (192.168.1.116, GPU)
systemd: papergen-<id>.service
marimo run --base-url /thesis/<id>
port 27000~27999
```
**왜 두 머신**: GPU(RTX Pro 6000)는 son-pro6000에만 있고, 외부 IP/DNS는 son-wtr에 묶여 있어서. son-pro6000의 마리모 프로세스가 데모 코드를 GPU에서 실행, son-wtr 의 nginx가 HTTPS 종료 + 리버스 프록시.
## 위치
**son-pro6000 (GPU 서버, papergen 본진)**:
- 프로젝트: `/home/son/prj/thesis`
- venv: `.venv` (Python 3.10.19, torch+CUDA+transformers)
- 출력 노트북: `output/paper_<id>.py`
- 상태 파일: `state/state.json` (포트 할당, 논문 메타)
- systemd unit: `/etc/systemd/system/papergen-<id>.service`
**son-wtr (웹 서버, 프록시 + cert)**:
- nginx site: `/etc/nginx/sites-enabled/learn.ericfromkorea.com`
- 논문별 location: `/etc/nginx/snippets/learn-papers/<id>.conf` (cross-machine via SSH)
- 인덱스 HTML: `/var/www/learn.ericfromkorea.com/thesis/index.html`
- TLS cert: `/etc/letsencrypt/live/learn.ericfromkorea.com/`
**SSH 백채널**: son-pro6000은 son-wtr로 키 인증 SSH 가능 (`son@192.168.1.121`).
publish.py가 nginx snippet 작성 + reload + index 갱신을 SSH로 수행.
**UFW (son-pro6000)**: 포트 27000~27999 LAN(192.168.1.0/24)만 허용. 외부 차단.
## 논문 추가 (반복)
```bash
ssh 192.168.1.116
cd ~/prj/thesis
.venv/bin/python main.py 2010.11929 --publish
# 자동으로:
# 1. arxiv 메타 + PDF 다운로드
# 2. PyMuPDF 텍스트 추출
# 3. Gemini 2.5-pro 호출 (Course schema, 5~7 레슨)
# 4. marimo .py 노트북 생성 (각 레슨에 viz + quiz 셀)
# 5. systemd unit 작성 (로컬), nginx snippet + index (son-wtr 원격)
# 6. 즉시 https://learn.ericfromkorea.com/thesis/2010.11929/ 에서 접근 가능
```
## 발행 취소
```bash
ssh 192.168.1.116
cd ~/prj/thesis
.venv/bin/python -c "from papergen.publish import unpublish; unpublish('1706.03762')"
```
## 디버깅
- **셀 실행 에러**: `ssh 192.168.1.116 sudo journalctl -u papergen-<id>.service -n 100`
- **HTTPS 상태**: `curl -sI https://learn.ericfromkorea.com/thesis/<id>/`
- **WebSocket 직접 검증** (셀이 정말 실행되는지):
```python
# son-pro6000에서 .venv/bin/python으로
import asyncio, json
from websockets.client import connect
async def t():
async with connect("ws://127.0.0.1:27000/thesis/<id>/ws?session_id=x") as ws:
for _ in range(50):
print(json.loads(await asyncio.wait_for(ws.recv(), 5))[:200])
asyncio.run(t())
```
- **셀 실행 일괄 테스트**: `marimo export html output/paper_<id>.py -o /tmp/x.html`
→ 모든 셀 서버 사이드 실행. stderr에 에러 표시. exit 0 이면 깨끗.
## 알려진 함정 / 학습된 교훈
- **chromium snap + AppArmor**: 헤드리스 스크린샷 안 됨 (WebSocket 차단). 실제 브라우저는
멀쩡함. WS handshake 직접 검증으로 대체.
- **LLM이 import 빠뜨림**: viz code 본문이 `plt`를 쓰는데 `import matplotlib.pyplot as plt`가
없으면 셀 실행 시 NameError. 해결: `blocks_viz.py`/`blocks_repro.py`가 `_run` 함수 본문에
`import torch, numpy as np, matplotlib.pyplot as plt`를 자동 prepend.
- **LLM이 dropdown 옵션에 따옴표 포함**: `["'It'", "'was'"]` 같이 보낼 때가 있음. 프롬프트에
명시했지만 100% 막을 수는 없음. 의심되면 옵션 list를 sanitize 추가 가능.
- **multi-line 문자열 금지**: prompt에 명시. triple-quoted 문자열을 쓰면 cell-body indenter가
문자열 내부에 공백 끼워 마크다운/문자열 내용을 망가뜨림.
- **포트 범위 27000~27999**: state.json이 정한다. UFW에 LAN 허용 룰 한 번만 추가하면 끝.
- **150줄 룰**: 모든 모듈 150줄 미만. 가장 긴 게 publish.py (~110줄), prompts.py (~110줄).
## 관련 스킬
- `tn-namecheap-dns-provision` — 새 서브도메인 추가 + DNS 전파 대기 (cert 발급 전).
- `tn-namecheap-dns` — 단순 A/TXT 레코드 조작.
- `tn-https-lets-encrypt-nginx` — cert 발급 트러블슈팅.
- `tn-nginx-reverse-proxy-config` — nginx 설정 일반.