Communitygithub.com

eternova/장기-영상-강의-노트

Claude Code 스킬: 장시간 동영상 강의를 자동으로 캡처하여 그림과 글이 포함된 Feishu 문서로 변환합니다. video→canvas 프레임 캡처 + AI 인식 + lark-cli 푸시.

장기-영상-강의-노트란 무엇인가요?

이 스킬은 30분 이상의 긴 동영상 강의(라이브 방송 다시보기, 온라인 강의, 강연 등)를 자동으로 분석하여 그림과 글이 포함된 요약 노트를 생성합니다. HTML5 video 요소를 사용하는 모든 플랫폼(샤오어통, 생재유술, 빌리빌리, 왕이윈커탕, 더우인 교육 등)을 지원하며, 기본 출력 대상은 Feishu 문서이고 로컬 마크다운 파일로도 내보낼 수 있습니다. Playwright MCP 브라우저를 통해 video 요소에 접근하여 canvas.drawImage로 프레임을 캡처하고(DRM 우회), AI가 캡처한 프레임을 분류(슬라이드, 강사 얼굴, 에디터 등)하여 중요한 슬라이드 위주로 추가 캡처합니다. 최종적으로 구조화된 마크다운 문서를 생성하고 lark-cli를 통해 Feishu 문서로 발행하며, 각 이미지에 정확한 타임코드를 포함시킵니다. 이 과정은 ffmpeg 다운로드(403 금지)나 전체 영상 다중 모달 분석(비용 과다) 같은 비효율적인 방식을 피하고, 브라우저 내 프레임 캡처가 핵심 원리입니다.

지원 대상✓Claude Code~Codex CLI✓Cursor
npx skills add eternova/long-video-course-notes

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

미리보기

스킬 README에서

placeholder-frame-XXXXs.jpg

문서

장기 동영상 강의 그림+글 노트 스킬

사용 시기

  • 사용자가 긴 동영상 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
![placeholder-frame-XXXXs.jpg](placeholder-frame-XXXXs.jpg)
(핵심 관점 + 핵심 문장 + 체크리스트)

...

## 초보자를 위한 실행 가능한 다음 단계
(주 단위 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이며, 사용자가 수동으로 어디든 보낼 수 있습니다.

출력 규격

각 성공적인 실행 후, 최종 응답은 반드시 다음을 포함해야 합니다:

  1. Feishu 문서 URL(바로 클릭 가능)
  2. 캡처한 총 프레임 수 + 총 재생 시간
  3. 섹션별 목차(사용자가 미리 볼 수 있도록)
  4. "다음 단계 제안" 한 문장(예: "댓글로 확장이 필요한 부분을 알려주세요.")

함께 제공되는 스크립트

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

관련 스킬