AIのデモは至る所にある。単一のPythonスクリプトで静止画の顔を入れ替えることができ、その結果は魔法のように見える。しかし、実際のユーザーがアップロードし、一度離れても、後で戻ってきて使えるようなものを作るとなると、それは全く別の仕事だ。最近、アニメーションGIFと参照用の顔画像を受け取り、全フレームの顔を入れ替えた同じアニメーションを返すウェブツールを構築した。視覚的な重労働はモデルが行うが、真のエンジニアリングの労力は、それを囲むアーキテクチャ、つまり接続を維持し、ページの更新に耐え、2分間の推論ジョブが504 Gateway Timeoutで消えてしまわないようにすることに注がれた。

これこそが、プロトタイプと製品の間のギャップである。エンジニアは拡散モデルのアーキテクチャや推論パラメータについて語るのが大好きだ。しかし、ユーザーが200フレームもある高密度のGIFをアップロードしたとき、ブラウザのタブが30秒で諦めてしまうようでは、誰もモデルのことなど気にしない。推論そのものと同じくらい、推論を取り巻くインフラストラクチャが重要なのである。

「待機」に伴う問題

標準的なウェブアーキテクチャは、高速なレスポンスを前提としている。ユーザーがボタンをクリックし、サーバーが応答し、ページが更新される。数十フレームに及ぶGIFの顔入れ替えは、その前提を即座に打ち砕く。モデルは私のサーバーではなくReplicateのGPU上で動作するため、長いGIFの処理には1分以上かかることも珍しくない。これほど長い間HTTPリクエストを維持しようとすれば、トラブルを招くことになる。ロードバランサーはアイドル状態の接続を切断し、ブラウザはネットワークエラーだと判断してリトライを繰り返す。ユーザーは固まったスピナーを眺めながら、アプリが壊れたと思い込んでしまう。

私は、リクエストの送信と結果の受け取りを切り離すことで、この問題を完全に回避した。ユーザーがGIFと顔画像をアップロードすると、Next.jsのバックエンドがReplicateでの予測を開始し、即座にジョブIDを返す。ユーザーはすぐに確認を得られる。実際の処理結果は、GPUの作業が完了した後、Webhookを通じて後から届く。このパターンは珍しいものではないが、長時間実行されるメディア処理においては極めて重要だ。これにより、予測不能な待ち時間が、「ジョブは受理されました。完了したら通知します」という確信を持てるハンドシェイクへと変わる。

信頼できるステートマシンを構築する

非同期処理を採用したら、可視性が必要になる。ユーザーはページを更新するだろう。タブを閉じて開き直すかもしれない。リンクをコピーして同僚に送り、その同僚が2時間後に確認するかもしれない。何が起きたのかを記録する永続的な仕組みがなければ、混乱を招くだけだ。

私は「信頼できる唯一の情報源(single source of truth)」としてSupabaseを使用した。アップロードのたびに一意のジョブIDを持つ行が作成され、その行は「queued」「processing」「succeeded」「failed」「expired」といった特定のステートを経て遷移していく。ユーザーが最初に送信ボタンを押すと、行はqueuedになる。Replicateが予測を受け付けた瞬間にprocessingへと移行する。Webhookがsucceededまたはfailedへと更新する。また、コールバックがないまま長時間放置されたジョブのためにexpiredを追加し、システムがいつまでも存在しないジョブを追いかけ続けないようにした。

TypeScriptで構築されたフロントエンドは、短い間隔でSupabaseをポーリングし、現在のステートに基づいて描画を行う。このポーリングは原始的に見えるかもしれないが、リフレッシュの問題を完全に解決してくれる。進捗状況を保持しているのはブラウザのメモリではなくデータベースであるため、ユーザーはノートPCを閉じて明日開いたとしても、状況がどこまで進んでいるかを正確に確認できる。Supabaseはクレジットも管理しているため、会計処理もステートを追跡するジョブレコードと紐付いた状態を維持できる。すべてが一箇所に集約されているのだ。

ブラウザに負荷をかけずにファイルを扱う

GIFは人々が考えているよりも重い。AIパイプラインには最適なサイズであっても、ファイルサイズが数メガバイトに及ぶことがある。それをブラウザ内でループ再生のプレビューとしてレンダリングすると、特に低スペックのデバイスではパフォーマンスが著しく低下する。そこで、AI用とユーザーインターフェース用の、完全に分離された2つのファイルパイプラインが必要になった。

プレビュー用には、WebAssemblyにコンパイルされたFFmpegを使用して、大きなGIFをアニメーションWebPに変換している。これは完全にブラウザのクライアントサイドで実行される。その結果、元のファイルに触れることなく、インターフェースの軽快さを保つ軽量なプレビューが実現できる。Replicateに送信されるGIFはそのままの状態に保たれる。この分離は重要だ。プレビューの圧縮ノイズが学習データや最終的なレンダリングに紛れ込むことは避けたいし、AIが考えている間にユーザーインターフェースが数メガバイトの巨大なデータで停止してしまうことも避けたいからだ。

FFmpeg WASMは、アップロードがブラウザを離れる前に、その他のGIF処理も行う。フレーム数の解析、寸法の検証、破損したファイルの早期検知などに使用している。GPUクレジットを消費する前に問題を検知することは、コストとユーザーの忍耐の両方を節約することにつながる。

デモを、人々が信頼できるソフトウェアへと変える

ここには、市場にあるほぼすべての生成AIツールに当てはまる、より広範な教訓があります。モデル自体は、作業全体の30%に過ぎないかもしれません。残りの70%は、誰もツイートすることのない、地味な裏方の作業です。状態の復元、Webhook署名、ファイル変換、クレジット管理、そして適切なエラー処理といった作業です。

私のスタックはシンプルかつ意図的です。Next.jsとTypeScriptがインターフェースとAPIルートを担い、Replicateがモデルを実行します。Supabaseは状態、データ、クレジットを管理します。FFmpeg WASMはクライアントサイドのメディア処理を担当し、Animated WebPがUIの高速化を維持します。各要素には一つの役割があり、それらは壊れやすく長時間持続するリクエストではなく、明示的な状態遷移を通じて接続されています。

ユーザーがクレジットを消費してフェイススワップを行うとき、彼らが求めているのは研究論文ではなく、信頼性です。もし生成に失敗したなら、システムはそれを検知し、ユーザーに伝えるべきです。ユーザーが待機している間は、眺めるための軽量なプレビューがあり、ブラウザを再起動しても状態が維持されるべきです。こうした細部へのこだわりは、うまく機能しているときは目に見えませんが、機能しないときには致命的なものとなります。

フェイススワップ自体は鮮やかな仕掛けです。しかし、このツールが「本物」だと感じられるのは、ユーザーがアップロードを行い、一度離れたとしても、作業の続きから再開できるからです。それこそが、単なるAPIコールを、人々が実際に信頼できるソフトウェアへと変えるものなのです。