Communitygithub.com

eternova/long-video-course-notes

Claude Code スキル:長尺ビデオ講座を自動で図入り飛書ドキュメントノートに変換します。video→canvas フレームキャプチャ + AI 認識 + lark-cli プッシュ。

long-video-course-notes とは?

このスキルは、長尺(30 分以上)の教育系ビデオ(ライブ配信の録画、オンライン講座、講演など、主に PPT と講師解説で構成されるもの)から、自動的に図入りの要約ノートを生成し、飛書(Lark)ドキュメントとして出力する Claude Code スキルです。処理の核となるのは、ブラウザ上の HTML5 video 要素から canvas 経由でフレームをキャプチャする手法で、HLS/MPEG-DASH などのストリームやトークン認証の制約を回避しながら高品質なフレームを得られます。対応プラットフォームは小鹅通、生財有術、Bilibili、网易云课堂、抖音教育など、video タグを使用するあらゆる Web サイトです。全工程は Playwright MCP を用いたブラウザ操作、AI によるフレーム分類(PPT/講師/エディタ画面など)と密度調整、Markdown 記事作成、lark-cli による飛書ドキュメントへの画像挿入までを完全自動化します。

対応✓Claude Code~Codex CLI✓Cursor
npx skills add eternova/long-video-course-notes

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

プレビュー

スキル README より

placeholder-frame-XXXXs.jpg

ドキュメント

長尺ビデオ講座図入りノートスキル

いつ使うか

  • ユーザーから長いビデオ URL(30 分以上)が与えられ、全体を見る代わりに図入りサマリーが欲しい場合
  • ビデオがライブ配信の録画 / オンライン講座 / 講演など、主に PPT と講師解説で構成されるコンテンツ(エンタメ系短編ではない)
  • 出力先は飛書ドキュメント(デフォルト)またはローカル Markdown
  • 対応プラットフォーム:<video> HTML5 要素を持つあらゆる Web サイト(小鹅通、生財有術、Bilibili、B站、网易云课堂、抖音教育など)

いつ使わないか

  • ビデオが Flash / Silverlight / プライベートプレイヤーを使用(HTML5 video 要素がない)
  • ビデオが 15 分未満(直接見たほうが速い)
  • ビデオが純粋なエンターテインメント(音楽、Vlog、アニメ)—— フレームキャプチャに意味がない
  • ユーザーが発話の文字起こしを希望する場合 —— このスキルは ASR を行わないので、別途 whisper や飛書妙記を使用すること

核心理論(なぜ有効か)

ビデオストリームは通常 HLS / MPEG-DASH で、トークン認証が付いているため、ffmpeg で直接ダウンロードしようとすると 403 エラーになります。しかし、ブラウザ内の video 要素はストリームを復号済みのフレームバッファとして保持しているため、canvas.drawImage(video, ...) を使うことで任意の時点の PNG を取得でき、著作権チェーン全体を迂回できます。

video 要素 (DRM フリー復号後) → canvas.drawImage → toDataURL → ローカル jpg

処理フロー概要

Step 1: ビデオのアクセス可能性を評価(ログイン状態 / HTML5 video かどうか)
Step 2: プローブ—— duration / resolution / src を取得
Step 3: 粗いサンプリング(8~10 分ごとに 1 フレーム)
Step 4: AI でフレームを分類(PPT vs 講師頭部 vs エディタ画面 vs Q&A)
Step 5: PPT 密集区間を補完サンプリング
Step 6: Markdown 記事を作成
Step 7: 飛書にプッシュ(フォルダ作成 + ドキュメント作成 + 画像を順次挿入)

Step 1:ログイン / アクセス可否チェック

Playwright MCP ブラウザでビデオ URL を開く:

mcp__playwright__browser_navigate(url=ビデオURL)

確認項目:

  • ログインページにリダイレクトされた場合 → 下記「ログイン戦略」を参照
  • 直接コースページが表示された場合 → Step 2 へ

ログイン戦略(優先順位)

A. プラットフォームに既にログイン済み(最も一般的、操作不要) — Playwright MCP ブラウザは同一セッション内で cookie を保持します。ユーザーが最近 Playwright で同一ドメインにアクセスしてログインしていれば、cookie は残っています。そのままナビゲーションしてみます。

B. ユーザーが自分の Chrome にログインしている → cookie インポート — ~/.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 内の cookie が期限切れ(expires_utc フィールドを確認)。

C. 無人ログインが不可能な場合 → ユーザーに依頼 — キャプチャ / スライダー / WeChat スキャンコードなどは決して自動化できないので、素直にユーザーにログインしてもらいましょう:

  • 1 文で明確に指示:「Chrome で X を開いてログインし、完了したら教えてください」
  • 2 段落以上の説明や 4 つの選択肢は不要

絶対に陥らないこと:ログイン試行→cookie 期限切れ→ツール変更→ツールがまた使えなくなる→さらに別のツール、という無限ループ。最大 2 回試してもだめなら、Plan B(ユーザーが自分でスクリーンショットを送る)に即座に切り替え。

Step 2:ビデオ要素プローブ

ビデオページを開いた後、以下を実行:

() => {
  const v = document.querySelector('video');
  if (!v) return {error: 'video 要素がありません'};
  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 分ごとに 1 フレーム)をサンプリングし、全体のリズムを把握:

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 ツールでフレームを 1 枚ずつ確認(画像は自動表示されます)。各フレームにタグ付け:

  • slide —— PPT の文字が読み取れる → 必ずキャプチャ
  • editor —— Cursor / VS Code / ターミナル → キャプチャ(デモシーンとして有用)
  • talking_head —— 講師の顔だけ → スキップ
  • qa / chat —— コメントやチャットのやりとり → 1~2 枚を Q&A 代表として選択

密集補完サンプリング:連続する 2 フレーム間が 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:Markdown 記事を作成

記事の構成(コースの種類に応じて調整):

# コースタイトル · 完全復習ノート

> コース情報:講師 / 日付 / 時間 / プラットフォーム / 元のビデオリンク

## この記事の対象読者
(1 段落で、読者のペルソナと学べることを明示)

## コース全体の構成

00:00 - 08:00 オープニング 08:00 - 25:00 パート 1 ... ...


## パート 1 · ...
> ⏱ タイムコード:**16:00 – 25:00**

### サブセクション 1
> ⏱ **16:00** · スライド 9/38
![placeholder-frame-XXXXs.jpg](placeholder-frame-XXXXs.jpg)
(主要な見解 + キャッチフレーズ + チェックリスト)

...

## 初心者向けの次の実行可能ステップ
(週単位でアクションアイテムを分解)

## 付録 · 重要なリソースリンク

作成ルール:

  • 各画像プレースホルダーにはユニークなファイル名 placeholder-frame-{seconds}s.jpg を使用し、後で selection-with-ellipsis で位置を特定できるようにする
  • 各 PPT には正確なタイムコード(hh:mm または mm:ss)+ スライド番号を必ず記す
  • スライドの PPT 内容は表に直接転記し、要約しない(ユーザーは原文を見たい)
  • 悪い例 / 良い例 / チェックリストなどの構造化コンテンツは Markdown の表またはリストを使用
  • 文末には必ず「初心者向けの次の実行可能ステップ」を週単位で記載すること。これがないと記事は読み物で終わってしまう

Step 7:飛書にプッシュ

7.1 lark-cli の認証状態を確認

lark-cli auth status

このスキルを初めて実行する場合、以下のスコープを順に承認する必要があります(必要に応じて 1 つずつ追加):

  • 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

⚠️ スコープは必ず一度に 1 つだけ追加すること——複数スコープを同時にリクエストすると invalid scope エラーになります。

7.2 フォルダを作成(デフォルトはユーザーの個人スペースルート)

lark-cli drive +create-folder --name "<フォルダ名>"
# folder_token が返されるので控えておく

7.3 画像プレースホルダーを除去したクリーン版 Markdown を準備

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 画像を 1 枚ずつ挿入

各画像は、章タイトルやユニークなテキストをアンカーとして使用:

lark-cli docs +media-insert \
  --doc "<doc_id>" \
  --file "frame-XXXXs-MMMmSSs.jpg" \
  --type image \
  --selection-with-ellipsis "<ユニークな見出しテキスト>" \
  --align center \
  --caption "<タイムコード · 説明>"

アンカー規則:

  • 章のタイトルの最初の 15 文字(ユニークであれば十分)
  • 同じ章に複数の画像がある場合 → 異なる段落のユニークなフレーズを使用
  • 失敗した場合は start...end 形式:--selection-with-ellipsis "先頭段落...末尾段落"

7.6 失敗時の処理

  • サーバータイムアウト → 1 回リトライ。16k 文字を超えるドキュメント作成はタイムアウトしやすいので、記事を分割してもよい
  • 「ルートフォルダで作成する必要があります」エラー → folder-token のスペルミスまたは未渡し
  • 画像プレースホルダーが残っている → Markdown 内に 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 秒が安定です。症状としては「最初の 2 フレームは OK だが、3 フレーム目からすべて同じ画像になる」という現象が出ます。

❌ アンチパターン 4:スコープを一度にまとめてリクエストする

lark-cli auth login --scope "scope1 scope2 scope3" は invalid scope エラーになります。一度に 1 つずつ。

❌ アンチパターン 5:gstack browse でビデオ操作と飛書操作を同時に行おうとする

gstack browse daemon は bash セッションの切り替え時に不安定になります。Playwright MCP でビデオをキャプチャ + lark-cli で飛書操作が最もクリーンです。

❌ アンチパターン 6:cookie が期限切れなのに無理に試し続ける

expires_utc < 現在時刻 を確認 → ユーザーに「再ログインしてください」と直接伝える。5 種類の方法を試してはいけません。

よくあるユーザーからの質問への対応

「なぜ直接字幕を出してくれないの?」 回答:可能ですが、別途 whisper を実行する必要があり(追加で 5~10 分かかります)。ユーザーが明確に文字起こしを希望する場合は実行します。それ以外はデフォルトで図入りノートを提供します。

「なぜもっとフレームをキャプチャしないの?」 回答:1 フレームあたり約 200KB で、飛書ドキュメントの挿入には容量制限があり、視覚的にも疲れます。12~18 枚がスイートスポットで、2 時間の講義をカバーできます。

「飛書じゃなくてもいい?」 回答:デフォルトでは ~/course-frames/article/article.md と同ディレクトリの *.jpg を出力します。ユーザーは任意の場所に手動で配布できます。

出力仕様

正常に実行が完了した場合、最終レスポンスには以下を含めること:

  1. 飛書ドキュメント URL(クリック可能)
  2. キャプチャしたフレーム数 + 合計時間
  3. 章ごとの簡易目次(ユーザーがプレビューできるように)
  4. 1 文の**「次のステップの提案」**(例:「コメントでどのセクションを拡充してほしいか教えてください」)

付属スクリプト

scripts/capture-frames.js —— Playwright evaluate で使用するプローブ + フレームキャプチャ関数(mcp__playwright__browser_evaluate に貼り付けて使用)

scripts/decode-batches.py —— バッチの base64 を jpg にデコードしてディスクに書き出すスクリプト

scripts/insert-images.sh —— 飛書 lark-cli で画像をバッチ挿入するテンプレート

参考として、完全に実行したケース:examples/deepsea-2026-04-23.md

関連スキル