長尺ビデオ講座図入りノートスキル
いつ使うか
- ユーザーから長いビデオ 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-{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 を出力します。ユーザーは任意の場所に手動で配布できます。
出力仕様
正常に実行が完了した場合、最終レスポンスには以下を含めること:
- 飛書ドキュメント URL(クリック可能)
- キャプチャしたフレーム数 + 合計時間
- 章ごとの簡易目次(ユーザーがプレビューできるように)
- 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
