Cloudflare Workers AIを使って画像生成機能を製品の機能として組んだ時に考えたこと
Cloudflareには色々な機能がありますが、昨今はAIにも力を入れており色々なモデルがCloudflare Workersで動かすことができます。
その中で今回は画像生成(その中でもimage to image)を使って製品の機能として組み込んでみたのでその時に考えたことを軽いTIPS的な記事として書いておきます。
導入経緯(前提条件)
私はGoens(ゴエンズ)というサービスを運用しています。
平たく言うと高齢者向けのマッチングアプリだと思ってもらって結構です。(有名どころのマッチングアプリより緩い感じですが)
その中で様々な課題があるんですが、AIよる画像生成機能を入れた理由は以下です。
- 年配の方向けなので、どうしても若い人よりプロフィール画像の重要性という点に意識が少し低い方がいらっしゃる
- 自分自身をどう撮るとうまく撮れるかがあまりわかっていない方もいらっしゃる
- 異性のプロフィール画像にあまり写りのよくない画像ばかりあるとサービス全体の期待値が下がってしまう
- そもそも自分の画像をネットなどに上げるのに抵抗がある方もいらっしゃる
どうでしょうか?皆さんの親御さんもしくは祖父母を想像してみてください。
そこで、せっかく使っているユーザさんが少しでもいい体験を頂くために自身のプロフィール画像をうまくデフォルメされた画像をあげることで上記の課題を解消してサービスを使って頂こうかなと思って開発しました。
そのため、この画像生成機能はあくまで補助的であり、最悪動かなくってよいという前提のため設計しています。
Cloudflare Workers AIってそもそもどう?
これは機能をどこまで厳密に求めるかによりますが、導入経緯にも書いた通りあくまで補助的であり、とりあえず使えたらいいというレベルでは問題ありません。問題ありませんというのはまったく何も考えなくても使えるという意味ではなく、ちょっと工夫が必要です。その点について書いていきます。
料金
Cloudflare Workers AIは今回紹介するimage to imageの他にwisperやllamaなど有名どころのモデルも存在します。
それぞれモデルがありますが、料金に関してはAIを使った出力に依存するようです。
で、今回はimage to imageを使うので、モデルは「Stable Diffusion v1.5 img2img」です。
Cloudflare Workers AIはベータモデルは料金が無料なのでAI部分に関しては現状はまだ料金がかかってません。
プロンプト
画像を生成するにはサンプルような画像とプロンプトが必要です。
export interface Env {
AI: Ai;
}
export default {
async fetch(request, env): Promise<Response> {
// Picture of a dog
const exampleInputImage = await fetch(
"https://pub-1fb693cb11cc46b2b2f656f51e015a2c.r2.dev/dog.png"
);
const inputs = {
prompt: "Change to a lion",
image: [...new Uint8Array(await exampleInputImage.arrayBuffer())],
};
const response = await env.AI.run(
"@cf/runwayml/stable-diffusion-v1-5-img2img",
inputs
);
return new Response(response, {
headers: {
"content-type": "image/png",
},
});
},
} satisfies ExportedHandler<Env>;
サンプルコードには prompt
のパラメータしか投げてませんが、ドキュメントを見る限り以下のパラメータを渡せそうです。
- prompt(require)
- image
- mask
- num_steps
- strength
- guidance
これだけで画像は確かに生成は出来るっちゃ出来るんですが、AIが生成する成果物の質をあげるには重要なパラメータである negative_prompt
のパラメータがありません。
じゃあ投げれないの?というとドキュメントに無いですが色々試してみた結果から negative_prompt
をAI呼び出しのパラメータに含めてると効いてると思われます。なので以下のように投げると negative_prompt
を反映出来ます(ただし、公式に公開しているわけではないので注意ください)
const inputs = {
prompt: "Change to a lion",
negative_prompt: "low quality",
image: [...new Uint8Array(await exampleInputImage.arrayBuffer())],
};
こうなると他のパラメータもうまく渡せるような気がします。
出力の安定性
これは未だベータだからということかもしれませんが、Cloudflare Workers AIで「Stable Diffusion v1.5 img2img」のモデルを使用して生成すると真っ黒な画像いわゆる画像の生成が失敗してしまうことがあります。しかも生成結果はバイナリに返ってくるのでこれをうまく失敗した画像を把握で再出力するような処理を組む必要があります。
私の場合は画像の生成結果をそのまま以下のようにR2に保存しています。
const response = await this.env.AI.run('@cf/runwayml/stable-diffusion-v1-5-img2img', inputs);
const fileName = 'convert/' + crypto.randomUUID() + '.png';
const res = await this.env.TEMP_IMAGE_BUCKET.put(fileName, response, { httpMetadata: { contentType: 'image/png' } });
こうすることでAIの実行結果をそのままR2に保存するのですが、ここから真っ黒の画像を判定します。で、どうやるかというとすごい力技なんですが、真っ黒の画像は基本843byteという結構小さめな画像になるようなので、R2に保存した結果から判定します。(少しサイズチェックに余裕を持たせています)
return {
success: res.size >= 850
}
私たちのサービスでこんな小さい画像が送られてくることはないので、これくらいのチェック処理で判定すればなんとかなっています。
画像処理のためのメモリ
画像処理をするので画像を読み込むためにCloudflare Workersのメモリの消費が気になりました。実際にメモリがどうなっているかというわけではなく、Cloudflare Workersが使えるメモリは128MBが上限です。これは何をどうやっても変更することができません。
さらに注意が必要なのは1リクエストごとに128MBのメモリを使えるわけではなく、生きている1インスタンス毎です。
なので、画像処理するためのメモリはちょっと工夫しておく必要があります。じゃあどうやったかというとメモリの使用を増やすために単純にCloudflare Workersの数を増やせば良いということでAIの処理を実行するHandlerを別Workersとして構築しました。さらに別Workersで構築すると前述の生成結果がほしいので、返り値の型を解消するためにRPCを使ってCloudflare Workers Bindingsで構築します。
ただ、ローカルでAIを動かすにはCloudflareのアカウントが必要になるので、開発者がチームなどで多い場合は全員を登録するのはちょっと面倒です。なので、Bindingされる側のCloudflare Workersのパブリックなエンドポイントも用意しておいて、開発時はそこを呼ぶというような処理で作っています。
export class Img2ImgService extends WorkerEntrypoint<Env> {
async convert(value: { image: string, prompt: string, negative_prompt: string, strength: number }) {
const { image, prompt, negative_prompt, strength } = value
const inputImage = await fetch(image)
const inputs = {
prompt,
negative_prompt,
strength,
image: [...new Uint8Array(await inputImage.arrayBuffer())],
};
const response = await this.env.AI.run('@cf/runwayml/stable-diffusion-v1-5-img2img', inputs)
const fileName = 'convert/' + crypto.randomUUID() + '.png'
const res = await this.env.TEMP_IMAGE_BUCKET.put(fileName, response, { httpMetadata: { contentType: 'image/png' } })
return {
success: res.size >= 850,
imagePath: fileName,
}
}
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const auth = request.headers.get('x-example-auth');
if (!auth || auth !== EXAMPLE_AUTH) {
return new Response('Unauthorized', { status: 401 });
}
const input = await request.json();
const img2img = new Img2ImgService(ctx, env);
const result = await img2img.convert(input);
return new Response(
JSON.stringify(result),
{ headers: { 'Content-Type': 'application/json' } }
)
}
}
type Img2ImgServiceBinding = Service<Img2ImgService>
export interface Env {
IMG2IMG_SERVICE: Img2ImgServiceBinding
}
export const createImg2ImgService = (env: Env) => {
if (env.NODE_ENV !== 'development') {
return env.IMG2IMG_SERVICE
}
return {
convert: async (input: Parameters<(typeof env.IMG2IMG_SERVICE)['convert']>[number]) => {
const response = await fetch('https://example.workers.dev/', {
headers: { 'x-example-auth': 'xxxxxxxxxxxxxxxx' },
method: 'POST',
body: JSON.stringify(input),
})
const json = await response.json()
return json
},
}
}
function ConvertTest() {
img2ImgService.convert({
prompt: 'your prompt',
negative_prompt: 'your negative prompt',
strength: 0.6,
image: 'https://example.com/image.png',
})
}
こうすることでローカルの時はパブリックなAPIを叩いて、実際に動作するCloudflare WorkersはRPCで繋いで通信経路Cloudflare Workers内部の通信経路を使うようにしています。
さいごに
というわけでCloudflare Workers AIを使って製品の機能に組み込んだ内容を書いてみました。実際のコードはもっとエラーハンドリングしたりしたり、細かい処理をやってたりしますが、基本概念はこのコードベースに構築されています。
Cloudflare Workersだけで完結するというのは非常に大きいので作ってみて楽しかったというのはありますが、興味ある方は使ってみてはいかがでしょうか?
Discussion