YouTube→Obsidian自動変換ツールをGitHubで公開して汎用化した

前回、YouTube料理動画をObsidianのレシピノートに自動変換する仕組みを作った。~/scripts/ に直置きしたスクリプト2本で、とりあえず43本の料理動画を捌けるところまでは行った。あの後Claude Codeと一緒にコードレビュー、テスト追加、CI整備をやってGitHubで公開し、さらにレシピ専用ツールから汎用ツールへと作り変えたので、その過程を書いておく。

コードレビューと修正

まずClaude Codeに既存のスクリプトをレビューしてもらった。指摘されたのは主にこのあたり:

  • 文字起こしファイルの書き込みが非アトミック(途中で落ちると壊れたファイルが残る)
  • エラーハンドリングが雑(全滅しても正常終了してしまう)
  • mlx-whisperのインポートチェックが遅い

アトミック書き込みは tempfile.mkstemp で一時ファイルに書いてから rename する定番パターンに修正。エラー時の終了コードも直した。

テストとCI

外部依存(yt-dlp、mlx-whisper、ファイルシステム)をモックして25本のテストを書いた。pytest + GitHub Actionsで回している。mlx-whisperはApple Silicon専用なのでCI上ではインストールせず、モックだけで検証する構成にした。

地味にハマったのが、テスト用のモックテキストが短すぎてハルシネーション検出に引っかかる問題。「こんにちは」みたいな短い文字列を返すモックにしていたら、50文字未満で自動的にハルシネーション扱いになっていた。

Whisperのハルシネーション対策

実際に43本処理してみると、何本かでWhisperが同じフレーズを延々繰り返す「ハルシネーション」が発生した。無音や環境音だけの区間で起きやすい。

対策として is_hallucinated() 関数を入れた。チェックしているのは3つ:

  1. テキストが50文字未満(短すぎる)
  2. 同一フレーズが5回以上連続する正規表現パターン
  3. 句読点・記号の比率が80%超(内容がない)

運用の改善

細かいが実際に使ってみて効いた改善をいくつか。

  • 1件ずつ処理 — 最初は全文字起こしを一括でClaude CLIに渡していたが、処理が詰まって進まなくなった。1件ずつループに変更し、成功したらdoneディレクトリに移動、失敗したらそのまま残してリトライ可能にした
  • Ctrl-Cで止まるようにする — ループ処理にしたら中断しても次のファイルに進んでしまう。trap INT でシグナルを捕捉して即座に終了するようにした
  • Macが重くならないようにする — WhisperとClaude CLIを同時に動かすとファンが回りっぱなしになる。nice -n 10 で優先度を下げて、caffeinate -i でスリープを防止
  • Obsidianとの共存_transcripts/ だとObsidianのファイルエクスプローラに表示されてしまう。.transcripts/ に変更してドットプレフィックスで非表示にした

GitHubで公開

ここまで育てたスクリプトを youtube-to-obsidian としてGitHubに上げた。~/scripts/ にはシンボリックリンクだけ残す構成。curl 一発でセットアップできるインストールスクリプトも用意した。

curl -fsSL https://raw.githubusercontent.com/nobu666/youtube-to-obsidian/main/install.sh | bash

Claude Code のスキル(グローバルコマンド)としても登録できるので、どのプロジェクトで作業中でも /youtube-to-obsidian で呼び出せる。

レシピ専用から汎用ツールへ

公開した時点ではまだ recipe というコマンド名で、プロンプトも料理動画専用だった。でも文字起こし部分は完全に汎用だし、変換プロンプトを差し替えればレシピ以外にも使える。ということで汎用化することにした。

コマンド名とプロンプトの切り替え

コマンド名を recipe から youtube-to-obsidian に改名。変数名も RECIPE_DIROUTPUT_DIR のように汎用的なものに揃えた。

プロンプトは prompts/ ディレクトリに複数置けるようにして、-p オプションで切り替える方式にした。

# デフォルト(汎用ノート形式)
youtube-to-obsidian https://www.youtube.com/watch?v=XXXXX

# レシピ
youtube-to-obsidian -p recipe https://www.youtube.com/watch?v=XXXXX

# 講義ノート
youtube-to-obsidian -p lecture https://www.youtube.com/playlist?list=XXXXX

用意したプロンプトは5種類。

プロンプト用途出力例
default汎用(構造化ノート)要約 + セクション分け
recipe料理動画 → レシピ材料リスト + 手順
lecture講義・セミナー → 要約ノート要旨 + キーポイント + 詳細ノート
workout筋トレ・ヨガ → メニュー表種目テーブル + フォーム解説
toolツール解説 → 手順書セットアップ + 使い方 + Tips

出力先をプロンプトごとに分ける

レシピと講義ノートと筋トレメニューが同じフォルダに入るのは嫌なので、プロンプトファイルのヘッダで出力先を指定できるようにした。

output_dir: ~/Library/Mobile Documents/.../Obsidian/Vault/YouTube/レシピ
---
上の文字起こしをObsidianレシピ形式に変換して...

output_dir: ヘッダで出力先を指定し、--- 以降がClaudeに渡されるプロンプト本文。フォルダがなければ自動作成する。-o オプションで一時的に上書きもできる。

全体の構成

前回の記事ではシンプルな直列パイプラインだったが、プロンプト切り替えと出力先分岐が加わってこうなった。

youtube-to-obsidianの構成図: YouTube→文字起こし→Claude CLI+プロンプト→Obsidian各フォルダ

字幕優先で爆速化

汎用化したので講義動画を試してみたら、問題が発覚した。Whisperでの文字起こしが遅すぎる。20分の動画に10分以上かかる。料理動画は5〜10分のものが多かったからギリギリ許容範囲だったが、1時間の講義には使い物にならない。

解決策はシンプルで、YouTubeの字幕を先に取りに行くようにした。日本語の動画ならほとんどの場合、手動字幕か自動生成字幕がある。字幕があれば数秒で取得できるので、Whisperの出番はほぼなくなった。

文字起こしのフォールバックチェーン: YouTube字幕→Whisper→説明欄

あわせてWhisperのモデルをlarge-v3からlarge-v3-turboに変更した。前回の記事でmediumからlarge-v3に上げた経緯を書いたが、large-v3は精度はいいものの遅い。large-v3-turboは精度をほぼ維持したまま速度が改善されている。もっとも字幕優先にしたことで、Whisperが動くケース自体がほとんどなくなったのだが、フォールバック先として速いに越したことはない。

実際に試してみた

3種類の動画で試した結果。

ツール解説動画(obsidian-skills) — 字幕から取得、数秒で文字起こし完了。セットアップ手順、使い方、Tipsがきれいに分かれたノートになった。

筋トレ動画(腹筋スーパーセット) — 字幕から取得。種目テーブル + フォームのポイントという形式で出力された。「膝は絶対に曲げない(曲げると効きが激減)」みたいな実践的な注意点もちゃんと拾っている。

講義動画(堀江貴文 拓殖大学講演 1時間) — 字幕から取得、文字起こし自体は速いがClaude CLIでの構造化に2〜3分。要旨 + キーポイント + 詳細ノート + 引用という構成で、1時間の講演がきれいに整理された。Whisperで文字起こしから始めていたら30分以上かかっていたはずなので、字幕優先の効果が一番大きいのはこういう長尺動画だ。

振り返り

前回の記事を書いた時点では ~/scripts/ に直置きした料理専用スクリプトだったものが、GitHubで公開された汎用ツールになった。レビュー、テスト、CI、コマンド改名、プロンプトシステム、字幕優先化、モデル変更。これを全部Claude Codeとの対話で進めた。

特に字幕優先への変更は、講義動画を実際に試してみなければ気づかなかった。料理動画だけで使っているうちはWhisperの速度が問題にならなかったので、用途を広げたことで初めて見えたボトルネックだった。「汎用化したら新しい課題が見つかり、それを解決したらさらに良くなった」というのは、ものを作っていて一番楽しい瞬間かもしれない。

youtube-to-obsidian — Apple Silicon Macと Claude Code があれば動く。プロンプトを追加すればどんな動画でもObsidianノートにできるので、自分の用途に合わせてカスタマイズしてほしい。