動画生成を、動画モデルに任せるのをやめた話
きっかけは単純な問いだった。Veo 3、Google AI Studio、Railway、Hermes Agent。この4つを組み合わせれば、AIが勝手に動画を作り続けるループが組めるんじゃないか。手元の道具を並べてみて、そう思った。
結論から言うと、組める。ただ、調べていくうちに前提が2つ崩れた。
ひとつは、そもそもVeo 3が既に無いということ。旧モデルIDは2026年6月30日に停止していて、今から触るならVeo 3.1系になる。もうひとつは、Google AI Studioがランタイムではないということ。あれはキーを発行してプロトタイプを試す場所で、プログラムから回す対象ではない。ループの実体は結局Gemini APIを直接叩くことになる。
そして、一番効いたのがコストだった。Veo 3.1にAPIの無料枠は用意されていない。音声なしの最安ティアで秒あたり$0.03、フル音声版なら$0.40。8秒のクリップが最安でも$0.25前後、Fastなら$1.2前後になる。日に10本を無人で回すと、安いほうを選んでも月$75を超える。
無料枠を積み上げて運用してきた自分にとって、この数字はかなり異質だった。自動化の話をしていたはずが、いつのまにか課金枠の話になっている。そこで方針を変えることにした。
無料でやるなら、動画モデルを叩くのをやめる
代わりに考えたのは、素材を静止画で作って、動きはffmpegで付けるという構成だ。Ken Burns効果、つまり画像に対してゆっくりズームとパンをかけて、シーン間をクロスフェードでつなぐ。そこにナレーションを乗せて字幕を焼き込む。
地味に聞こえるかもしれない。でも、note やショート動画で量産されているAIコンテンツの実態は、かなりの割合でこれだ。そして何より、完全にゼロ円で回る。
ローカルで動画モデルを回す選択肢は最初から無かった。オープンウェイトの動画生成モデルは、12GBのGPUに載る最軽量クラスがLTX-Videoの0.9.5あたりで、まともな品質を求めるなら24GB以上が要る。自分のLenovoは実効メモリ5.8GB、GPUは検出されていない。Hugging FaceのZeroGPU Spacesを経由する手もあるが、混雑とクォータに左右されるので、日次で回す土台としては脆すぎると判断した。
組んだ構成
実行基盤はGitHub Actionsにした。publicリポジトリなら実行時間の無料枠を気にしなくていいし、ubuntu-latestのランナーにはffmpegもPythonも最初から入っている。
台本の生成はOpenRouterの無料モデル、具体的にはdeepseek-chat-v3-0324:free。画像はCloudflare Workers AIのFlux 1 Schnellで、これは以前から無料枠で使っていて実績がある。音声はKokoro-onnxを第一候補にして、失敗したらedge-ttsに落とす二段構え。Kokoroは82Mパラメータと小さいのでCPUで十分回るし、ライセンスもApache 2.0で気兼ねがない。
そして合成がffmpeg。ここが実質的な「動画生成エンジン」になる。
デプロイ先は無い、という発見
途中で自分でも混乱したのだが、この構成にはデプロイ先が存在しない。
最初はRailwayを想定していた。Veoは投げてから数分待つ非同期APIなので、待ち受けるプロセスが要る。だからPaaSが必要になる、という理屈だった。でも無料構成に切り替えた瞬間、その待ち時間が消えた。待ち受ける相手がいなくなれば、常駐先を用意する理由もまるごと消える。
GitHub Actionsはデプロイ先ではなく、実行環境そのものだ。cronの時刻になるとUbuntuのコンテナが立ち上がり、スクリプトを走らせてmp4を吐いて、そのまま消える。置き場所を決める必要がない。
ちなみに、VercelやCloudflare Pagesではこれは無理だった。Vercel Functionsの実行時間はHobbyで30秒、Proでも300秒。Hobbyのcronは1日1回が上限で、実行時刻も1時間の幅でしかブレを保証しない。1分の動画に画像8枚とTTSとエンコードを積むと、どう詰めても数分かかる。それ以前に、これらのランタイムはffmpegバイナリを実行する前提で作られていない。
疎結合にしておく
パイプラインは5つのスクリプトに割った。plan、images、tts、render、publish。それぞれ単体で実行できて、受け渡しはout/配下のファイルだけで行う。
plan.jsonがあり、img/に画像があり、audio/に音声とtiming.jsonがある。render.mjsはそれを読んでfinal.mp4を吐く。この入出力の契約さえ守れば、render.mjsの中身が何であってもいい。
つまり将来、やっぱりVeoを使いたくなったとき、差し替えるのはrender.mjsだけで済む。今の無料構成は妥協ではなく、後から交換できる部品として置いてある。そう設計しておくと、選択が「今は無料でいく」という判断になり、「無料しか選べない」という制約にならない。
止める場所を先に決めておく
このシステムで一番こだわったのは、生成の質ではなく、承認ゲートの位置だった。
Linearのagent-readyというラベルが付いて、ステータスがTodoになっているIssueだけを、plan.mjsが拾う。ラベルを貼れるのは人間だけ。逆に言えば、ラベルを貼らなければ何も動かない。
エージェントは止まらない。指示が曖昧でも最後まで走り切ってしまう。だから止める場所を先に決めておく必要がある。ラベル1枚を「着手してよい」の合図にするだけで、暴走したときのコストが劇的に下がる。
完成した動画も、GitHub Releaseにdraftとして上がるだけで自動公開はしない。人間が見て、良ければ出す。全自動が偉いのではなく、どこを人間が握るかを設計したものが強い、と今は思っている。
つまずいた場所
APIキーの取得が、思った以上に面倒だった。特にCloudflareは、アカウントIDがダッシュボードのURLに埋まっている32桁の英数字だと分かるまで少し迷った。APIトークンのほうは、作成直後の1画面にしか文字列が表示されない。閉じたら二度と見られない。既に「Workers AI」という名前でトークンを作ってあったのに、値を控えていなかったので結局作り直した。
権限はAccount → Workers AI → Readの1行だけで足りる。Global API Keyは絶対に使わないほうがいい。あれはアカウント全体を操作できてしまうので、publicリポジトリ絡みの作業で万一漏れたときの被害が比べものにならない。
ワークフローの細部にも罠があった。日本語フォントをapt-getで入れておかないと、字幕が全部豆腐になる。動画は完成するのに読めない、という一番がっかりする失敗の仕方をする。Kokoroのモデルファイルは数百MBあるので、actions/cacheで挟んでおかないと毎朝ダウンロードし直すことになる。しかもcacheは、依存をインストールする手前に置かないと意味がない。後ろに置くと、毎回落としてから「保存しました」と言うだけになる。
concurrencyのcancel-in-progressも、trueのままだと危なかった。レンダリング中に手動実行を重ねた瞬間に前の実行が殺される。plan.mjsが既にIssueをclaimした後だと、Linear上に誰も触っていないIn Progressが残る。バッチ処理ではfalseのほうが安全だと判断した。
artifactのアップロード条件も直した。元の書き方だとplan.mjs自体がコケたときにフラグが立たず、artifactが上がらない。一番ログが欲しい場面で証拠が取れないのは、さすがにもったいなかった。
いま思っていること
まだ1本も動画を出していない。Linearに初回のIssueを1件立てたところで、ここから実際に回してみる段階だ。
ただ、組んでいる途中で一番面白かったのは、技術的な部分ではなかった。「無料でやる」という制約を先に置いたら、設計そのものが変わったことのほうだ。課金前提なら、動画モデルに投げて待つだけの、単純だが平坦な構成になっていたと思う。無料に縛ったせいで、素材と合成を分けて考えることになり、結果として差し替え可能な構造になった。
制約は、たぶん敵ではない。少なくとも今回は、そう感じている。
いいなと思ったら応援しよう!
最後まで読んでいただき、ありがとうございます♡ もし気に入っていただけたなら、お気軽に「スキ」してくださると嬉しいです。ものすごく元気が出ます。