장기 동영상 강의 그림+글 노트 스킬
사용 시기
- 사용자가 긴 동영상 URL(>30분)을 제공하고, 단순히 시청하는 대신 그림과 글이 포함된 요약을 원할 때
- 동영상이 라이브 방송 다시보기 / 온라인 강의 / 강연처럼 PPT와 강사 설명이 주를 이루는 콘텐츠(오락용 단편 아님)
- 출력 목표가 Feishu 문서(기본값) 또는 로컬 마크다운일 때
- 동영상 플랫폼이
<video>HTML5 요소를 사용하는 모든 웹사이트(샤오어통, 생재유술, 빌리빌리, B站, 왕이윈커탕, 더우인 교육 등) 지원
사용하지 않는 시기
- 동영상이 Flash / Silverlight / 비공개 플레이어를 사용(HTML5 video 요소 없음)
- 동영상 길이 < 15분(그냥 보는 게 더 빠름)
- 동영상이 순수 오락(음악, 브이로그, 애니메이션) — 프레임 캡처가 무의미
- 사용자가 음성 대본을 원할 때 — 이 스킬은 ASR을 하지 않으므로, whisper나 Feishu妙记를 별도로 사용해야 함
핵심 원리(왜 이 방식이 통하는가)
동영상 스트림은 일반적으로 HLS / MPEG-DASH이며 토큰 인증이 있어서 ffmpeg로 직접 다운로드하면 403 오류가 발생합니다. 하지만 브라우저 내의 video 요소는 이미 스트림을 복호화하여 프레임 버퍼로 만들어 놓았기 때문에, canvas.drawImage(video, ...)를 사용하면 모든 시점의 PNG를 얻을 수 있어 저작권 체인 전체를 우회할 수 있습니다.
video 요소 (DRM-프리 복호화 후) → canvas.drawImage → toDataURL → 로컬 jpg
전체 프로세스 개요
Step 1: 동영상 접근 가능성 평가(로그인 상태 / HTML5 video 여부)
Step 2: 프로브 — 재생시간 / 해상도 / src 획득
Step 3: 개략 샘플링(8-10분 간격으로 한 프레임)
Step 4: AI가 프레임 분류(PPT vs 강사 얼굴 vs 에디터 vs Q&A)
Step 5: PPT 밀집 구간에 추가 샘플링
Step 6: 마크다운 문서 작성
Step 7: Feishu에 푸시(폴더 생성 + 문서 생성 + 이미지 하나씩 삽입)
Step 1: 로그인 / 접근 가능성 확인
Playwright MCP 브라우저로 동영상 URL 열기:
mcp__playwright__browser_navigate(url=동영상URL)
확인 사항:
- 로그인 페이지로 리다이렉트 → 아래 '로그인 전략' 참조
- 강의 페이지가 바로 보임 → Step 2로 이동
로그인 전략 (우선순위)
A. 플랫폼 이미 로그인됨 (가장 일반적, 추가 작업 불필요) — Playwright MCP 브라우저는 동일 세션 내에서 쿠키를 유지하므로, 사용자가 최근에 Playwright로 같은 도메인에 방문하여 로그인한 기록이 있으면 쿠키가 남아 있습니다. 바로 탐색 시도.
B. 사용자가 자신의 Chrome에서 로그인 → 쿠키 가져오기 — ~/.claude/skills/gstack/browse의 cookie-import-browser 사용:
cd ~/.claude/skills/gstack/browse
bun run src/cli.ts goto "https://target.com/" # 먼저 대상 도메인에 접속해야 함
bun run src/cli.ts cookie-import-browser chrome --domain target.com
⚠️ 실패 흔한 원인: 사용자 Chrome의 쿠키가 이미 만료됨(expires_utc 필드 확인).
C. 무인 로그인 불가능 → 사용자에게 요청 — 캡차 / 슬라이더 / 위챗 QR코드 등은 절대 자동화 불가능하므로, 그냥 사용자에게 로그인을 요청:
- 명확한 1문장 안내: "Chrome에서 X를 열고 로그인한 후, 저에게 알려주세요."
- 2문장 이상 설명하지 말고, 4가지 옵션을 주지 마세요.
절대 빠지지 말아야 할 함정: 로그인 시도→쿠키 만료→도구 변경→도구 또 실패→또 도구 변경의 무한 루프. 최대 2번 시도 후 바로 Plan B로 하향 조정(사용자가 직접 스크린샷을 찍어 나에게 보내기).
Step 2: 비디오 요소 프로브
동영상 페이지를 연 후 실행:
() => {
const v = document.querySelector('video');
if (!v) return {error: 'no video element'};
return {
src: v.src || v.currentSrc,
duration: v.duration,
width: v.videoWidth,
height: v.videoHeight,
paused: v.paused,
readyState: v.readyState,
title: document.title,
url: location.href
};
}
판단:
duration > 0이고readyState >= 3→ 프레임 캡처 가능src가blob:...URL → MSE 사용, 정확히 우리가 원하는 경우videoWidth >= 720→ 프레임 품질 충분readyState < 3→ 동영상이 로드되지 않음, 재생 버튼을 먼저 눌러 로드를 트리거해야 할 수 있음(muted 자동 재생이 더 안정적)
Step 3: 개략 샘플링 주요 시간 지점
N분 동영상에 대해 먼저 ceil(N/8) 프레임을 샘플링(8분 간격), 전체 리듬을 파악:
async () => {
const v = document.querySelector('video');
v.pause(); v.muted = true;
const duration = v.duration;
const interval = 480; // 8분
const targets = [];
for (let t = 30; t < duration - 30; t += interval) targets.push(Math.floor(t));
const seekTo = (t) => new Promise((resolve) => {
const onSeeked = () => { v.removeEventListener('seeked', onSeeked); resolve(); };
v.addEventListener('seeked', onSeeked);
v.currentTime = t;
setTimeout(() => { v.removeEventListener('seeked', onSeeked); resolve(); }, 8000);
});
const canvas = document.createElement('canvas');
canvas.width = v.videoWidth;
canvas.height = v.videoHeight;
const ctx = canvas.getContext('2d');
window.__frames = window.__frames || {};
const out = [];
for (const t of targets) {
await seekTo(t);
v.pause();
await new Promise(r => setTimeout(r, 800)); // 프레임 버퍼 대기
ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
window.__frames[t] = canvas.toDataURL('image/jpeg', 0.8);
out.push({t: v.currentTime, target: t, kb: (window.__frames[t].length / 1024) | 0});
}
return out;
}
핵심 세부 사항:
v.muted = true: 자동 재생 감지 차단 방지v.pause(): seek 후 다시 한 번 호출(일부 플레이어는 seek 후 자동 재개)- 800ms 대기: seeked 이벤트가 발생해도 프레임이 렌더링되었음을 보장하지 않으므로, GPU 버퍼에 약간의 시간을 주어야 함. 그렇지 않으면 이전 시간 지점의 프레임이 캡처될 수 있음
window.__frames에 저장: 한 번에 30MB의 거대한 base64를 반환하지 않음
Step 4: base64 추출하여 디스크에 쓰기
각 evaluate 호출은 최대 ~5MB JSON만 반환. 배치 처리:
() => {
const keys = Object.keys(window.__frames).sort((a,b) => +a - +b);
const out = {};
for (const k of keys.slice(START, START+4)) {
out[k] = window.__frames[k].split(',')[1]; // data:image/jpeg;base64, 제거
}
return out;
}
mcp__playwright__browser_evaluate의 filename 매개변수로 JSON을 ~/.playwright-mcp/ 또는 ~/에 저장. /tmp에 직접 쓸 수 없음(샌드박스 제한).
디스크 저장 스크립트:
import json, base64, os
os.makedirs(os.path.expanduser("~/course-frames"), exist_ok=True)
for batch in glob("~/course-frames/batch*.json"):
d = json.load(open(batch))
for t, b64 in d.items():
secs = int(t)
fn = f"frame-{secs:04d}s-{secs//60:03d}m{secs%60:02d}s.jpg"
open(f"~/course-frames/{fn}", "wb").write(base64.b64decode(b64))
Step 5: AI가 프레임 분류 + 추가 샘플링
Read 도구를 사용하여 프레임을 하나씩 확인(이미지가 자동으로 표시됨). 각 프레임에 레이블 지정:
slide— PPT 텍스트 있음, 읽을 수 있음 → 반드시 캡처editor— Cursor / VS Code / 터미널 → 캡처(데모场景 유용)talking_head— 강사 얼굴만 있음 → 건너뛰기qa/chat— 댓글 인터랙션 → 1-2장 Q&A 대표로 선택
밀집 구간 추가 샘플링: 연속된 두 프레임 사이의 간격이 8분이지만 내용이 완전히 다른 경우(예: Principle 01에서 Principle 05로 바로 넘어감), PPT 전환이 빈번한 구간이므로 돌아가서 추가 샘플링:
// [start, end] 범위에서 1-2분 간격으로 추가 샘플링
const targets = [];
for (let t = START; t <= END; t += 90) targets.push(t);
Step 6: 마크다운 문서 작성
문서 구조(강의 유형에 따라 미세 조정):
# 강의 제목 · 완전 복습 노트
> 강의 정보: 강사 / 날짜 / 시간 / 플랫폼 / 원본 동영상 링크
## 이 글은 누가 보면 좋을까요
(1문단, 독자 프로필 + 배울 내용)
## 강의 전체 구조
00:00 - 08:00 오프닝 08:00 - 25:00 Part 1 ... ...
## Part 1 · ...
> ⏱ 타임코드: **16:00 – 25:00**
### 하위 섹션 1
> ⏱ **16:00** · 슬라이드 9/38

(핵심 관점 + 핵심 문장 + 체크리스트)
...
## 초보자를 위한 실행 가능한 다음 단계
(주 단위 Action Items)
## 부록 · 주요 리소스 링크
작성 규칙:
- 각 이미지 placeholder는 고유한 파일명
placeholder-frame-{seconds}s.jpg사용, 이후selection-with-ellipsis로 위치 찾기 용이 - 각 PPT마다 정확한 타임코드(hh:mm 또는 mm:ss) + 슬라이드 번호 반드시 표시
- PPT 슬라이드 내용은 표에 직접 옮겨 적기, 요약하지 않기(사용자는 원문을 보길 원함)
- 반례/정례 / 체크리스트 같은 구조화된 내용은 마크다운 표 또는 목록 사용
- 문서末尾에 반드시 「초보자를 위한 실행 가능한 다음 단계」를 주 단위로 나누어 작성하지 않으면 문서는 읽기 전용에 불과함
Step 7: Feishu 푸시
7.1 lark-cli 인증 상태 확인
lark-cli auth status
이 스킬을 처음 실행할 때는 다음 scope가 필요함(필요에 따라 하나씩 추가):
space:folder:create— 폴더 생성docx:document:create— 문서 생성docx:document:write_only+docx:document:readonly— 문서 수정(미디어 삽입 필요)docs:document.media:upload— 이미지 업로드
인증 명령:
nohup lark-cli auth login --scope "<scope>" > /tmp/lark-auth.log 2>&1 &
sleep 5
cat /tmp/lark-auth.log # device-flow URL 획득
open "<URL>" # 사용자가 브라우저에서 인증하도록 함
# 사용자 완료 대기
until grep -q "인증 성공" /tmp/lark-auth.log; do sleep 3; done
⚠️ scope는 반드시 한 번에 하나씩만 추가해야 함 — 여러 scope를 함께 요청하면 invalid scope 오류 발생.
7.2 폴더 생성(기본값: 사용자 개인 공간 루트)
lark-cli drive +create-folder --name "<폴더명>"
# 반환된 folder_token을 기록
7.3 이미지 placeholder가 없는 깨끗한 마크다운 준비
sed '/!\[.*\](placeholder-frame/d' article.md > article-clean.md
7.4 문서 생성
cd ~/course-frames/article # markdown 경로는 반드시 cd로 들어가야 함(CLI 제한)
lark-cli docs +create \
--markdown @article-clean.md \
--folder-token "<folder_token>" \
--title "<문서 제목>" \
> /tmp/doc-create.json 2>&1
# doc_id 추출
7.5 이미지 하나씩 삽입
각 이미지는 섹션 제목 또는 고유 텍스트를 anchor로 사용:
lark-cli docs +media-insert \
--doc "<doc_id>" \
--file "frame-XXXXs-MMMmSSs.jpg" \
--type image \
--selection-with-ellipsis "<고유 섹션 제목 텍스트>" \
--align center \
--caption "<타임코드 · 설명>"
Anchor 규칙:
- 섹션 제목 앞 15자 사용(고유하면 충분)
- 같은 섹션에 여러 이미지 → 서로 다른 단락의 고유 구문 사용
- 실패 시
start...end형식 사용:--selection-with-ellipsis "첫 문단...마지막 문단"
7.6 오류 처리
- 서버 타임아웃 → 1번 재시도. 16k 문자를 초과하는 문서 생성은 타임아웃이 자주 발생하므로, 문서를 두 부분으로 나누는 것도 방법임
- "루트 폴더에 생성해야 함" 오류 → folder-token이 잘못되었거나 전달되지 않음
- 이미지 placeholder가 남아 있음 → 마크다운에
placeholder-frame-XXX가 잔존, 먼저--mode overwrite로 문서의 깨끗한 버전을 다시 쓴 다음 이미지 삽입
안티 패턴 (함정 피하기)
❌ 안티 패턴 1: ffmpeg로 직접 동영상 다운로드 시도
HLS + 토큰은 거의 항상 403 오류가 발생합니다. 설령 다운로드가 가능하더라도, 전체 동영상을 다운로드하는 데 20분, ffmpeg로 프레임을 추출하는 데 5분이 추가로 소요됩니다. video → canvas: 5초 만에 16프레임 캡처.
❌ 안티 패턴 2: Claude에게 전체 동영상을 보도록 시킴
당신에게는 그런 능력이 없습니다. 설령 가능하더라도(멀티모달 비디오 API), 비용은 프레임 캡처의 50배입니다. 프레임 캡처 + 이미지 확인 + 타임스탬프 위치 지정이야말로 레버리지입니다.
❌ 안티 패턴 3: seek 후 대기 시간 없이 drawImage 실행
seeked 이벤트 발생 ≠ 프레임 렌더링 완료. GPU 버퍼에는 200-500ms의 지연이 있습니다. 최소 800ms, 1초가 더 안정적입니다. 증상은 "처음 두 프레임은 괜찮은데, 세 번째 프레임부터 모두 같은 이미지입니다."
❌ 안티 패턴 4: scope 한 번에 모두 요청
lark-cli auth login --scope "scope1 scope2 scope3"은 invalid scope 오류를 발생시킵니다. 한 번에 하나씩.
❌ 안티 패턴 5: gstack browse로 동영상 캡처와 Feishu 작업을 동시에 실행
gstack browse 데몬은 bash 세션 전환 시 불안정합니다. Playwright MCP로 동영상 캡처 + lark-cli로 Feishu 작업이 가장 깔끔합니다.
❌ 안티 패턴 6: 쿠키가 만료되었는데도 계속 시도
expires_utc < 현재 시간 확인 → 즉시 사용자에게 "다시 로그인하세요"라고 알리고, 5가지 방법을 시도하지 마세요.
일반적인 사용자 질문 대비
"왜 바로 동영상 자막을 주지 않나요?" 답변: 가능하지만, whisper를 별도로 실행해야 합니다(추가 5-10분 소요). 사용자가 명시적으로 대본을 원하면 실행하고, 그렇지 않으면 그림+글 노트가 기본값입니다.
"왜 더 많은 프레임을 캡처하지 않나요?" 답변: 각 프레임은 ~200KB이며, Feishu 문서에는 크기 제한이 있고 시각적 피로도 고려해야 합니다. 12-18장이 적정하며, 2시간 수업을 커버합니다.
"Feishu 말고 다른 곳에 보낼 수 없나요?"
답변: 기본 출력은 ~/course-frames/article/article.md + 동일 디렉토리의 *.jpg이며, 사용자가 수동으로 어디든 보낼 수 있습니다.
출력 규격
각 성공적인 실행 후, 최종 응답은 반드시 다음을 포함해야 합니다:
- Feishu 문서 URL(바로 클릭 가능)
- 캡처한 총 프레임 수 + 총 재생 시간
- 섹션별 목차(사용자가 미리 볼 수 있도록)
- "다음 단계 제안" 한 문장(예: "댓글로 확장이 필요한 부분을 알려주세요.")
함께 제공되는 스크립트
scripts/capture-frames.js — Playwright evaluate에 사용되는 프로브 + 프레임 캡처 함수(mcp__playwright__browser_evaluate에 붙여넣기)
scripts/decode-batches.py — 디스크에 배치 base64 → jpg 저장
scripts/insert-images.sh — Feishu lark-cli 배치 이미지 삽입 템플릿
완전히 실행된 사례 참조: examples/deepsea-2026-04-23.md
