なぜ「Webから無料で」にこだわったか
APIキーで叩けばいい話だ、と思う人もいるだろう。ただ僕の場合、月5万円収益化を目指しながら初期コストをゼロに近づける必要があった。GeminiのAPI無料枠はRPMが低い。しかも画像生成(Imagen)はAPIでは別料金になる。
Gemini 2.0 Flash を gemini.google.com のWebインターフェース経由で使えば、プロプランに入っていなくても画像生成を含めて叩ける。KDP表紙やブログサムネを毎日量産する僕にとって、これは見逃せない。
最終的に「BraveをCDP(リモートデバッグ)で操作して自動化する」という構成に落ち着いた。その経緯と詰まりポイントを正直に書いておく。
CDPでBraveを操作する構成
Playwright で独自の user_data_dir を立てる方法は最初に試して捨てた。理由は単純で、Googleアカウントのセッションが毎回切れるから。
Brave を --remote-debugging-port=9222 で起動して、既存のログイン済みセッションをそのまま流用する方式に切り替えた。
# launch_brave_debug.sh の骨格
/Applications/Brave\ Browser.app/Contents/MacOS/Brave\ Browser \
--remote-debugging-port=9222 \
--profile-directory="Default" \
&
これを launchd で常駐させている。com.taito.brave-cdp というラベルで、マシン起動時に自動でBraveが上がる。
接続側はこう書く:
from playwright.sync_api import sync_playwright
from cdp_browser import connect_cdp_sync
with sync_playwright() as pw:
browser, ctx = connect_cdp_sync(pw)
page = ctx.new_page()
page.goto("https://gemini.google.com/")
# ...
browser.close()
connect_cdp_sync は cdp_browser.py に集約してある。ポートが死んでいたら例外を出してDiscordに通知する。
画像生成の自動化で詰まった3点
プロンプトに日本語を混ぜると壊れる
最初に試したプロンプトはこれだった:
「Python自動化で月5万円稼いだ実録」というタイトルのサムネを作って
結果は文字化け or 生成失敗。何回やっても同じ。
原因は Gemini の画像生成エンジンが日本語テキストをレイアウトに組み込む処理が不安定なのかどうか、正確には分からない。ただ経験則として「プロンプトは英語で書き、入れたい日本語テキストを明示的に指定する」形式が一番安定している。
Create a YouTube thumbnail with dark background and white text saying "Python自動化で月5万円稼いだ実録".
Style: tech blog, clean, minimalist.
Text must be clearly legible Japanese characters.
これで文字化けが大幅に減った。完全には消えないが。
セレクタが定期的に変わる
gemini.google.com のDOM構造は何の予告もなく変わる。先週動いていたセレクタが今週は None を返す。
対策として、テキストエリアへの入力は page.get_by_role("textbox") のようなアリアベースセレクタを優先するようにした。クラス名での直指定はメンテコストが高すぎる。
textarea = page.get_by_role("textbox", name="Gemini へのメッセージを入力してください")
textarea.click()
textarea.fill(prompt)
page.keyboard.press("Enter")
それでも崩れるときは崩れる。スクリーンショットをキャプチャしてDiscordに送り、手動対応を促す仕組みも入れた。
生成完了の検出タイミング
「生成中」スピナーが消えたかどうかを wait_for_selector で待つ実装にしたが、スピナー要素のクラス名も変わる問題がある。
今は「ダウンロードボタンが出現するまで待つ」方式に変えた。画像が完成しないとダウンロードボタンは出ないので、これが一番信頼できる。
page.wait_for_selector('[aria-label="画像をダウンロード"]', timeout=60000)
タイムアウトは60秒に設定している。それ以上かかることはほぼない。かかるとしたらネットワーク障害か、Gemini側の何かだ。
1日の運用実態
実際のパイプラインはこういう流れになっている:
- KDP表紙やブログサムネのプロンプトが JSON ファイルに溜まる
generate_eyecatch_for_posts.pyが朝7時に起動- Brave CDP 経由で Gemini Web を開く
- プロンプトを投入 → 画像生成 → ダウンロード
- ファイル名を整えて
02_Content/blog_thumbnails/に保存 - WordPress の対象記事にアイキャッチとして紐付け
1日20回を上限として運用している。それ以上叩くとレート制限がかかるか、ブラウザ操作がflake になる。体感では15〜18回あたりが安定ゾーン。
無料運用の限界、正直に言うと
画像の品質は高い。KDP表紙として普通に使えるレベルで、Pollinationsとは別次元だ。ただ無料でWeb経由という構成には明確な限界がある。
まず「壊れる」前提でコードを書く必要がある。セレクタ変更・レート制限・ページレイアウト変更、これらは定期的に起きる。エラー検出とDiscord通知を最初から入れないと、気づかないまま画像ゼロの記事が公開される。
次に、生成速度が読めない。API経由なら応答時間がほぼ安定するが、Web経由だとGemini側の負荷によって10秒で終わることもあれば50秒かかることもある。バッチ処理に組み込むとき、タイムアウト設計が難しい。
それと、Googleがいつ仕様を変えてこの抜け道を塞ぐかも分からない。実際、過去にも似たような無料活用パスがアップデートで消えた事例はある。「いつ壊れてもいいように、APIキー経由のフォールバックを用意しておく」のが正しい姿勢だと思っている。今はまだそこまで実装できていないが。
Watchdog を入れてから安定した
brave_cdp_watchdog.py というスクリプトを追加して、BraveのCDPポートが死んでいたら自動再起動するようにした。
import subprocess, requests, time
def check_cdp():
try:
r = requests.get("http://localhost:9222/json", timeout=3)
return r.status_code == 200
except:
return False
def restart_brave():
subprocess.run(["pkill", "-f", "Brave Browser"])
time.sleep(2)
subprocess.Popen(["/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
"--remote-debugging-port=9222", "--profile-directory=Default"])
これを launchd で5分おきに監視させている。Braveがクラッシュしてもパイプラインが止まらなくなった。以前はクラッシュに気づかず3時間分の画像生成が全部スキップされていた。
まとめ
Gemini 2.0 Flash のWebから無料で使う構成は、コストゼロで高品質な画像生成をパイプラインに組み込めるという点で今のところ機能している。ただ「安定している」とは言えない。壊れることを前提に、壊れたら通知して手動対応できるフローを用意しておくのが現実的な落とし所だ。
完璧なシステムより、壊れたときに素早く直せるシステムの方が長続きする。それが今の僕の結論✨