📤

Geminiの会話をObsidianに自動保存するツールをPythonで作った話(失敗だらけの開発経緯)

に公開

はじめに

Gemini(GoogleのAI)の会話を手動でMarkdown保存し、Obsidianのバックアップ用フォルダに溜めていく作業が面倒だったので、「取得件数だけ指定したらあとは自動でエクスポートしてくれるツール」をPythonで作りました。

やっていることは**「RPAならすぐできるレベル」**なのに、ブラウザの起動の仕方・画像クリック・2画面・ダウンロード先・UIのトグルなどで何度もつまずきました。

この記事では、開発の経緯と失敗、どう直したかをまとめます。同じような自動化(ブラウザ+画像クリック+ファイル移動)を試す方の参考になれば幸いです。


なぜ Obsidian に、なぜ Gemini の会話を?

わたしは日々のメモ・調べもの・読んだことを Obsidian に集約しており、リンクや検索で後から参照できる「ナレッジベース」として使っています。ローカルの Markdown なので長期保存やバックアップも安心です。

vault は Google ドライブで同期しており、PC で編集した内容がスマホの Obsidian にも反映されるようにしているので、スマホ・PC の両方で同じノートを確認できます。この運用方法はかなり使えるので、いずれ記事にまとめる予定です。

その Obsidian の vault を Cursor のワークスペースに含めておくことで、コードを書くときにノートを参照したり、逆にノートからコードへ飛んだりしやすくしています。

そこで Gemini の会話も Obsidian に入れておきたい、というのが動機です。

Gemini ではアイデア出し・コードの下書き・調査メモが混ざった会話が溜まりますが、サービス側にだけ置いておくと履歴が流れていくし、過去の議論を「あのときのあの話」として参照しづらい。

エクスポートした Markdown を Obsidian に置いておけば、他のメモや 02_Literature のようなフォルダと一緒に検索・リンクでき、Cursor で作業するときにも過去の会話をすぐ参照できます。

だから「手動で保存して Obsidian に溜める」を続けていたのですが、それが面倒になって自動化ツールを作った、という流れです。


やりたかったこと

  • 入力: 取得するチャット数(例: 10件)、飛ばす固定数(例: 2件)
  • 処理: Gemini上で会話一覧を開き、指定件数だけ順に「エクスポート → Markdown」を実行し、ダウンロードされた _*.md を指定フォルダ(Obsidianの02_Literatureなど)に移動する
  • 運用: ユーザーは「ChromeでGeminiを開いておく」だけ。ツールは接続してクリック・キー送信・ファイル移動まで自動で行う

失敗①: ブラウザをツールから起動しようとしてハマった

やったこと

最初は「一切手動で触りたくない」と考え、PlaywrightでChromeを起動し、GeminiのURLまで開く方式にしました。

  • launch_persistent_context で既存のユーザープロファイル(ログイン済み)を指定して起動
  • すると「既存プロファイルでChromeを起動しています…」のままハング
  • 別スレッドで起動すると今度は greenlet の "Cannot switch to a different thread" が出る
  • 諦めて通常の chromium.launch() にすると起動はするが、毎回ログインが必要でつらい

気づき

「起動済みのChromeは操作できない」わけではない。
pyautogui は「今フォーカスがあるウィンドウ」にキー送信・画像クリックできる。つまり ユーザーが普段どおりChromeでGeminiを開いておき、ツールは「接続」と「キー・クリック」だけやればよい という設計にすればよかった。

やった修正

  • 「ユーザーがChromeでGeminiを開く」 を仕様にした
  • ツールは CDP(Chrome DevTools Protocol)で既に起動しているChromeに接続するだけ
  • 起動用に chrome_debug.bat(中でPowerShellの chrome_debug.ps1 を呼ぶ)を用意し、--remote-debugging-port=9222--remote-allow-origins=* でChromeを起動してもらう
  • ユーザーの手順は「Chromeを全部終了 → chrome_debug.bat → Geminiを開く → gemini-exporter.bat」に一本化

失敗②: CDP接続がずっと「接続できませんでした」

やったこと

  • 「Chromeを閉じてからOK」とダイアログを出し、そのあと connect_over_cdp("http://127.0.0.1:9222") を実行
  • 何度やっても 接続タイムアウト

原因

  • 既に普通のChromeが起動している状態で chrome_debug.bat を実行していた
  • その場合、新しいプロセスは起きず「既存のChromeにウィンドウが1つ増える」だけになり、デバッグポートは立たない
  • バッチの start "" %CHROME% --remote-debugging-port=9222 ... が、Windowsの解釈で引数がChromeに渡っていない可能性もあった

やった修正

  • chrome_debug.bat の先頭で taskkill /F /IM chrome.exe を実行し、Chromeを確実に終了してから起動
  • 起動は PowerShell に任せ、Start-Process -ArgumentList @("--remote-debugging-port=9222", ...) で引数を確実に渡す(chrome_debug.ps1
  • Chrome 109以降では --remote-allow-origins=* がないとCDPで弾かれることがあるので追加

失敗③: 画像クリックで「pygetwindow がインストールされていません」「pyautogui がインストールされていません」

やったこと

  • ウィンドウのアクティブ化に pygetwindow、キー・画像クリックに pyautogui を使用
  • ログには「インストールされていません」と出るが、requirements.txt には書いてある

原因

  • gemini-exporter.batcall venv\Scripts\activate.bat のあと python を呼んでおり、環境によっては 別のPython(venv でない) が使われていた
  • Pillow / pyscreeze / OpenCV が入っておらず、画像検索(locateOnScreen(..., confidence=0.7))が使えなかった

やった修正

  • バッチで venv の python を直接指定: "%~dp0venv\Scripts\python.exe" -m src.main
  • pip install pygetwindow pyautogui Pillow opencv-python を実行して依存関係をそろえた

失敗④: 「Gemini」でウィンドウを探したらバッチ窓が前面に出た

やったこと

  • タイトルに「Gemini」を含むウィンドウを前面に出す処理を実装
  • すると 「ウィンドウをアクティブにしました: gemini-exporter.bat」 と出て、コマンドプロンプトが前面に来てしまう

原因

  • ウィンドウタイトルに「gemini」が含まれるため、バッチの窓がヒットしていた(大文字小文字を区別しない検索のため)

やった修正

  • 「Gemini」かつ「Chrome」 の両方を含むウィンドウだけを対象にするように変更
  • これでブラウザのウィンドウだけがアクティブになる

失敗⑤: 2画面だと画像クリックできなかった

やったこと

  • 主モニターでは locateOnScreen でMarkdownボタンがクリックできるが、サブモニターにChromeを出していると一切検出されない

原因

  • Windows では ImageGrab(pyautogui のスクリーンショット)が主モニターしかキャプチャしていない ため、region=(仮想スクリーン) を渡してもサブモニターが写っていない

やった修正

  • mss全モニターを1枚の画像としてキャプチャsct.monitors[0])し、その画像に対して pyautogui.locate() で検索
  • 見つかった座標に mon["left"], mon["top"] のオフセットを足してクリック
  • 2画面でもサブモニター上のボタンをクリックできるようになった

失敗⑥: 画像クリック後、マウスがボタンの上にあると次回検出できない

やったこと

  • 連続で何件もエクスポートするとき、2件目以降で「画面上でMarkdownボタン画像が見つかりません」 になることがあった

原因

  • クリック後もマウスがボタンの上にあると、ホバーでボタンの色が変わり、テンプレート画像と一致しなくなる

やった修正

  • Markdownボタンをクリックした直後に pyautogui.moveRel(-300, 0) でマウスを左に300pxずらす
  • ボタンからマウスが外れ、次回の画像検索が安定する

失敗⑦: Pythonで開いたChromeだとダウンロードが「変な場所」「変な名前」になる

やったこと

  • 手動でエクスポートすると「〇〇.md」のように保存されるが、ツール経由だと UUIDのような拡張子なしのファイル名 で、保存場所も分からない という事象が出た

原因

  • CDPで接続したChromeでは、ダウンロードのデフォルト保存先が別扱いになっていた
  • 拡張機能側の都合で、ファイル名が拡張子なしで渡されるケースがあった

やった修正

  • 接続直後に Page.setDownloadBehaviordownloadPathconfig.json のダウンロードフォルダに設定
  • 検出対象を「_で始まる .md」に加え、拡張子なしでUUID形式のファイル名も「新規ダウンロード」として検出し、_<UUID>.md にリネームしてから移動
  • さらに、Chromeが保存後にリネームすることがあるため、検出後に1.5秒待ってから再度フォルダをスキャンし、存在するファイルのうち最新のものを採用するようにした
  • _move_file 側でも、指定パスが存在しない場合は「直近15秒以内に更新された _*.md」を探して移動するフォールバックを入れた

失敗⑧: 会話一覧が既に開いているのにハンバーガーメニューを押して隠してしまう

やったこと

  • Geminiでは、ハンバーガーメニューを押すとサイドバーが開閉する(トグル)
  • 「とにかずメニューを押して会話一覧を出そう」としていたため、既に開いているときに押してしまい、一覧が隠れて次の会話をクリックできなくなる ことがあった

やった修正

  • 会話一覧(a[data-test-id="conversation"])が既に表示されているか を判定する is_chat_list_visible() を追加
  • 表示されているときはハンバーガーメニューを押さない
  • 判定は2回行い、その間に300ms待つ。初回だけの判定だと「まだ描画されていない」と誤判定することがあるため、二重チェックで「本当に非表示のときだけ」メニューを押すようにした

失敗⑨: ダウンロードを検出したのに「ファイルが見つかりません」で移動できない

やったこと

  • 「ダウンロードを検出: _〇〇.md」とログに出るが、その直後に移動処理で FileNotFoundError になる

原因

  • Chromeや拡張が、保存後にファイル名をリネームしている(例: 末尾スペースの有無、一時名→正式名)
  • 検出した瞬間のパスと、実際にディスクに存在するパスが一致しなくなる

やった修正

  • 新規 _*.md を検出したあと 1.5秒待ち、フォルダを再スキャンして 実際に存在する _*.md のうち 最も mtime が新しいもの を返すようにした
  • 移動処理(_move_file)では、指定パスが存在しない場合に「同じフォルダ内の直近15秒以内に更新された _*.md」を探し、それを移動するフォールバックを追加

現在の運用フロー(まとめ)

  1. chrome_debug.bat を実行 → Chromeがデバッグポート9222で起動(既存Chromeは終了)
  2. そのChromeで Gemini のトップページ を開く(ログイン済み)
  3. gemini-exporter.bat を実行 → 取得件数・スキップ数(前回値がデフォルト)を入力
  4. ダイアログ「Chromeをデバッグモードで…」でOK → ツールがCDP接続し、会話を順に開いてショートカット送信・Markdownボタン画像クリック・ダウンロード待ち・指定フォルダへ移動を繰り返す
  5. 監視モードに入り、以後 _ で始まる .md がDownloadsに出現すれば同フォルダへ自動移動

使っている技術

用途 技術
ブラウザ操作(会話一覧の取得・クリック) Playwright(CDP接続)
ウィンドウの前面化 pygetwindow
ショートカット送信・画像クリック pyautogui
2画面での画像検索 mss(全モニターキャプチャ)+ pyautogui.locate
画像マッチの精度 OpenCV(confidence)、Pillow
ファイル監視・移動 watchdog
設定・前回値 config.json / state.json

まとめ

現在は安定稼働しており、手動で会話を保存して Obsidian に溜める面倒な作業が一つ消えた。

  • 「全部自動でやりたい」 からブラウザ起動までツール側でやろうとして、起動まわりで何度も失敗した。「ユーザーがChromeでGeminiを開く」 にしたら、CDP接続だけになってシンプルになった。
  • 起動済みChromeは pyautogui で操作できる ので、最初から「ユーザーが開く+ツールはキーとクリックだけ」でもよかった。
  • 2画面・ダウンロード先・リネーム・UIのトグルなど、想定外の挙動は「検出→待機→再スキャン」「フォールバック」 でだいたい吸収できた。
  • 同じような「ブラウザ+画像クリック+ファイル」の自動化をやる場合は、起動はユーザー任せ・接続と操作だけ自動 にすると、トラブルが減りやすいと思います。

失敗だらけでしたが、その分だけ「なぜ動かないか」を切り分ける経験が積めたので、記事として残しておきます。

Discussion