バックエンドでの処理
バックエンドはフロントエンドから呼び出され、複雑なロジックや処理を担う重要な役割を果たします。 例えば、Webサイトへのログイン認証や、ECサイトでの決済処理などもバックエンドで行われています。
クライアントサイドにすべての処理を記述すると、APIキーの漏洩などセキュリティリスクが生じる可能性があります。これを防ぐため、データベースや外部APIへのアクセスコードはバックエンドに隠蔽する必要があります。
また、重い処理をクライアントサイドで行うと、デバイスに負荷がかかりパフォーマンスが低下します。バックエンドは効率的なリクエスト処理や負荷分散を行い、複数ユーザーへの対応やシステム全体の拡張性を確保します。さらに、ビジネスロジックを集約することでコードの一貫性とメンテナンス性を向上させます。
最新画像のメタデータ取得 (GET /icongen/current/metadata)
トップページから呼び出しているエンドポイントです。 最新画像のメタデータを取得します。
キャッシュ
メタデータの取得はキャッシュから取ってくるようにしています。
トップページを開くたびに SELECT * FROM images ORDER BY created_at DESC LIMIT 1 とやると DB に負荷がかかるはずなので、Cloudflare Workers KV (高速にデータの読み書きができる一時保存場所) にキャッシュしています。まあ負荷がかかるほどのアクセス数はないのですが……。
2024-12-23 EDIT: あずきバーさんからツッコまれたのですが、インデックス効くので高速で動くらしいです。キャッシュ要らなかった。 → このPRで削除しました!: https://github.com/trpfrog/trpfrog.net/pull/97
メタデータの検索 (GET /icongen/query)
ここでは画像のメタデータを検索します。 現時点では「プロンプトに含まれる文字列」のみで検索できるようにしています。
Cloudflare D1 (データベース) に SQL クエリを投げるだけなので特に複雑なこと/面白いことはやっていません。 強いて書くなら今回初めて Drizzle ORM を使いました。
基本的な API は SQL に近い感じなのと、drizzle.config.ts 以外の独自の設定ファイルなしで使えるのがよかったです。
あんまりコードジェネレータが好きではないので、型計算だけでほぼ全てが完結するところが美しいなあって思います。
欠点を挙げるとすると細かいクエリを書く部分が tree-shakable な感じの API (伝われ) なのでいまいち補完が効きにくくてつらかったです。
実装はこんな感じです。そんなに難しいことせず素直に書けました。 そういえば今回バックエンドではクリーンアーキテクチャ (もどき) を採用したのですが、hono/context-storage が大活躍しました。ここで説明すると長くなるのであとで書きます。
画像生成 (POST /icongen/update)
画像生成リクエストを受け取り、画像生成を行います。 ここが一番複雑なことをやっています。簡単に説明すると次のような流れです。
- 最後の生成から3時間以上経過していたら画像生成を行う
- ランダムな単語 (お題の単語) を取得する
- LLM にお題の単語を入力してプロンプトを生成する
- 画像生成モデルにプロンプトを入力して画像生成を行う
- 生成した画像をオブジェクトストレージ (R2) に保存する
- メタデータをデータベース (D1) に保存する
- 最新データを KV Store (Workers KV) に保存する
- 生成した画像の URL とメタデータを返す
ランダムなプロンプトの生成
人間が介在しない完全なるランダムな画像を生成をするために GPT-4o を使ってプロンプトを生成をすることにしました。プロンプトとは画像生成 AI に渡すお題のことです。
GPT-4 に直接「なんかプロンプト作って!」とお願いしても良いのですが、「なんかプロンプト作って!」だけだといつか同じことを言う可能性があるので、もうちょっと明確にランダム性が欲しいです。そこで、ランダムに単語を10個取ってきて「これを使ってプロンプトを作って!」と言ってみることにしました。
ランダムにならないって本当?
感覚的に……おそらく温度パラメータ (次単語選択の確率に関係するパラメータ) を高めに設定してあげれば十分にランダムなプロンプトが得られるとは思います。とはいえ、言語モデルはランダムな値を出すことは構造的に苦手だと思うので、外部からランダム性を与えてあげるのが良いかと思っています。すみません、詳しく実験していないのでわかりません……。(シンプルな乱数に関しては偏りがあることが報告されています: https://llmrandom.straive.app/)
ランダムな単語は Rando というサービスから取れます。GET /api?words=10 で英単語をランダムに10個取れます。ありがとうございます。
次に「この単語を使って画像生成のためのプロンプトを作って!」というプロンプトを GPT-4o に突っ込みます。
GPT-4o に与えるプロンプト (和訳)
あなたのタスクは、Stable Diffusion向けの視覚的に魅力的で、創造的かつ一貫性のある画像生成プロンプトを作成することです。以下のステップに従って進めてください。
- フェーズ 1: 入力語の理解
- 提供された入力語を注意深く確認してください。
- すべての単語を使用する必要はありません。 最も魅力的で一貫性のあるシーンを作成するために単語を選びましょう。
- コンテンツガイドライン(例:健全であること、包括性)を遵守しつつ、ユーモアや視覚的な魅力を保ちましょう。
- フェーズ 2: 「基本」プロンプトの作成
- 目的: 入力語に基づいてシンプルでわかりやすい説明を作成します。
- ガイドライン: プロンプトは「an icon of trpfrog」で始め、入力語をいくつか取り入れてください。
- アイデア出し: この段階ではアイデアを探るため、長めのプロンプトでも問題ありません。
- フェーズ 3: 「創造的」プロンプトの展開
- 目的: ユニークで面白い、または予想外の要素を取り入れて、シーンを視覚的または概念的に魅力的にします。
- ガイドライン: プロンプトは「an icon of trpfrog」で始め、詳細を追加してシーンを広げます。
- 焦点: スタイル、修飾語、および追加の詳細を使用してシーンを強化しつつ、適切性と一貫性を保ちます。
- フェーズ 4: 「洗練された」プロンプトの作成
プロンプトの内容を簡単に説明すると、「入力された単語を使って次のような JSON を生成してね」という内容になっています。
このように複数のステップで画像生成プロンプトを作成することで、よりクリエイティブで魅力的な画像を生成することを目指しています。これが研究だったら意味のあることなのかしっかり調査する必要がありますが、今回は趣味なのでおまじない程度ということで……。でも OpenAI o1 や "Let's think step by step" で知られているように、LLM には思考する余裕を与えてあげることで、より良い結果が得られることが報告されています。
また、出力を JSON としたのは結果をプログラム上で扱いやすくするためです。
さて、このプロンプトを AI に与えて得られる JSON をパースして、画像生成のためのプロンプトとします。このとき、
のように AI の応答を Zod でバリデーションしています。 AI は確率的に振る舞うので、必ずしも正しい JSON を吐かないことに注意が必要です。とはいえ GPT-4o は十分に賢いので、few-shot learning だけでほぼ 100% 正しい JSON を吐いてくれます。ありがたいですね。 あと OpenAI API だと出力として JSON を吐くことを強制するオプションがあります。
画像を生成する
プロンプトさえできてしまえば画像は簡単に生成できます。
HuggingFace のライブラリ @huggingface/inference を使います。HuggingFace は多数の AI モデルをホストしているサービスです。AI 界の GitHub 的な存在です。
HuggingFace では推論をやってくれる API (Inference API) があり、@huggingface/inference はそのラッパーライブラリとなっています。API で推論できることの嬉しい点としては
- 簡単
- デカいメモリが必要ない (重要)
ことがあげられます。通常、AI の推論には巨大な RAM か VRAM が必要になる上、モデル自体のサイズも大きいのでそれなりのディスク容量が必要になります。 これをクラウドでやると莫大なお金がかかってしまいます。これを API にまとめて向こうで推論をやってくれる HuggingFace、感謝……
次のように推論を書きました。
雑にいえば hf.textToImage にモデル名とプロンプトを渡せば終了です。簡単!
モデル名にはちくわぶさんの作ってくださった Prgckwb/trpfrog-sd3.5-large-lora を指定します。
ところで、HuggingFace ではセンシティブな画像が生成されてしまったとき、エラーを吐いたり再生成したりすることなく、無言で真っ黒な画像を返すという最悪な仕様があります。
真っ黒な画像の base64 には ooooAKKKKACiiigA が繰り返し出現するので、これを利用してセンシティブチェックを行います。(これ本当にどうにかならないんですかね)
これで、ランダムな画像の生成ができるようになりました!
