🤖

Cloudflare Workers AI 使ってみた「画像生成編」

2024/11/20に公開

はじめに

こんにちは、ikechan こといけがわです。

皆さん、最近話題の生成AIを試したことはありますか?私は以前、ChatGPTに「自分の写真を元に、好きな筋トレと愛犬をテーマにしたプロフィール画像を作ってほしい」とお願いしてみたことがあります。すると、想像以上に素敵な画像ができあがり、それ以来ずっと愛用しています!

最近の生成AIの進化には驚かされますよね。普段は基本的にChatGPTやClaudeを使用してるんですが、「他にも面白いAIないかな?」と思いながらいろいろ調べているときに、Cloudflare Workers AI を見つけました。実は普段から個人開発で Cloudflare を利用しているのですが、このAIサービスの存在は知りませんでした。でも調べてみると、画像生成やテキスト解析などのAIモデルを簡単に使えるプラットフォームだと分かり、「これは試してみたい!」とワクワクしました。

また個人開発でAIを活用したサービスを作ろうとしているところだったこともあり、Cloudflare Workers AI がどれくらい便利でどんな成果を出せるのか、実際に試してみることにしました!

前提条件

この記事を進めるにあたり、以下の知識とツールが必要です:

  • Cloudflare アカウント
  • Node.js 開発環境(Bun を使用)
  • Hono フレームワークの基本操作

画像生成の実装手順

1. 環境設定

まず、必要なパッケージをインストールし、環境を整えます。

Hono のセットアップ

今回のプロジェクトでは、軽量で高速な Web フレームワーク Hono を使用します。セットアップ手順については、公式ドキュメントの Getting Started を参考にしてください。
https://hono.dev/docs/getting-started/basic
また、今回私は JavaScript ランタイムとして Bun を使用しますが、Node.js や Deno などお好きなランタイムを選んでいただいても問題ありません。

Bun を使用する場合は、以下のコマンドを実行してプロジェクトを初期化し、必要なパッケージをインストールしてください。

$ bun create hono@latest my-app
$ cd my-app

.env ファイルの作成

次に、プロジェクトルートに .env ファイルを作成します:

CLOUDFLARE_API_TOKEN=your-cloudflare-api-token
CLOUDFLARE_ACCOUNT_ID=your-cloudflare-account-id
OUTPUT_DIRECTORY=./generated_images
  • CLOUDFLARE_API_TOKEN: Cloudflare の API トークン(後述で取得方法を説明します)
  • CLOUDFLARE_ACCOUNT_ID: Cloudflare アカウント ID(Cloudflare ダッシュボードで確認可能)
  • OUTPUT_DIRECTORY: 画像を保存するディレクトリ

Cloudflare API トークンの取得

Cloudflare Workers AI を使用するには、Cloudflare アカウントと API トークンが必要です。以下の手順で取得してください。

  1. Cloudflare ダッシュボード にログインします。
  2. 「マイプロフィール → APIトークン」に遷移し、新しいトークンを作成します。
  3. API作成時は事前に構成された権限をまとめたテンプレートが用意されています。「Workers AI」のテンプレートを使用もしくは自身でカスタムトークンを作成して下さい。

    基本的には公式ドキュメント Create API tokenを参考にしていただければと思います。

https://developers.cloudflare.com/fundamentals/api/get-started/create-token/

Cloudflare Account IDの取得

基本的にアカウントIDはURLに記載されています https://dash.cloudflare.com/{accountId}
またダッシュボード「Workders & Pages」画面右側にもアカウントIDが表示されているので確認してみてください。

作成したトークンと``をコピーし、上記 .env ファイルに貼り付けます。

2. 画像生成の設定

Cloudflare Workers AI の dreamshaper-8-lcm モデルでは、以下のプロパティを使用して画像生成をカスタマイズできます:

必須パラメータ

  • prompt
    • 型: string
    • 用途: 生成したい画像の説明
    • 例: "A cute dachshund with a red ribbon"

画像サイズ設定

  • height
    • 型: integer
    • 範囲: 256 ~ 2048px
    • 用途: 画像の高さ指定
  • width
    • 型: integer
    • 範囲: 256 ~ 2048px
    • 用途: 画像の幅指定

生成制御パラメータ

  • negative_prompt
    • 型: string
    • 用途: 避けたい要素の指定
    • 例: "blurry images, low quality"
  • num_steps
    • 型: integer
    • デフォルト: 20
    • 用途: 拡散ステップ数(高いほど高品質)
  • guidance
    • 型: number
    • デフォルト: 7.5
    • 用途: プロンプトへの忠実度

画像変換パラメータ

  • strength
    • 型: number
    • 範囲: 0 ~ 1
    • 用途: img2img時の変更強度
  • seed
    • 型: integer
    • 用途: 再現性のための乱数シード

https://developers.cloudflare.com/workers-ai/models/dreamshaper-8-lcm/

3. 実装例

まず、画像生成の核となる関数を実装します:

const generateImage = async (prompt: string): Promise<Uint8Array | null> => {
  try {
    const response = await fetch(
      `${CLOUDFLARE_API_URL}/@cf/lykon/dreamshaper-8-lcm`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          prompt,
          negative_prompt: `
          blurry, low quality, rough, distorted, incorrect colors, unrelated animals, cats, birds, short-haired, short fur, smooth fur, 
          missing ribbon, wrong ear, missing ears, left ear ribbon, messy background, abstract elements, cartoonish, unrealistic lighting, 
          overexposed, underexposed, incorrect proportions, fat, overweight, thin, skeletal, aggressive, scary, sad, angry, dull, 
          wrong hair color, no bronze hair, black fur, gray fur, missing details, low contrast, muted colors, pixelated, low resolution,
          text, watermark, logo, extra limbs, extra ears, missing legs, cropped, out of frame, incomplete, wrong breed, toy dog, wolf, fox, only two ears.
          `,
          height: 512,
          width: 512,
          num_steps: 20,
          guidance: 1,
        }),
      }
    );

    if (!response.ok) {
      console.error(`Failed to generate image: ${response.statusText}`);
      return null;
    }

    const buffer = await response.arrayBuffer();
    return new Uint8Array(buffer);
  } catch (error) {
    console.error("Error generating image:", error);
    return null;
  }
};

実際にAIモデルがプロンプトを解釈する際、以下のような不要な要素が混じることがあります。

  • 他の動物(例: 猫や鳥)が生成されてしまう。背景が抽象的で、キャラクターがぼやけたり、不鮮明になる。...etc
    これらの問題を避けるために、先ほどプロパティ説明時にも記載があった、negative_promptを設定しています。内容は後ほどプロンプトとして送る「"A cute, long-bronz-haired dachshund with a red ribbon on its right ear.(右耳に赤いリボンをつけた、かわいい長毛のダックスフンド。)」に沿ったできるだけ忠実な画像を生成するための内容です。

次に、生成した画像を保存する関数を実装します:

const saveImage = async (filename: string, data: Uint8Array) => {
  try {
    if (!existsSync(OUTPUT_DIRECTORY)) {
      mkdirSync(OUTPUT_DIRECTORY, { recursive: true });
    }

    const filePath = path.join(OUTPUT_DIRECTORY, filename);
    await writeFile(filePath, data);
    console.log(`Image saved to ${filePath}`);
    return filePath;
  } catch (error) {
    console.error("Error saving image:", error);
    return null;
  }
};

4. 完全なソースコード

以下が完全な実装例です:

import { Hono } from "hono";
import { writeFile } from "fs/promises";
import { existsSync, mkdirSync } from "fs";
import path from "path";

const app = new Hono();
const CLOUDFLARE_API_URL = `https://api.cloudflare.com/client/v4/accounts/${process.env.CLOUDFLARE_ACCOUNT_ID}/ai/run`;
const API_KEY = process.env.CLOUDFLARE_API_TOKEN;
const OUTPUT_DIRECTORY = process.env.OUTPUT_DIRECTORY || "./generated_images";

// 画像生成
const generateImage = async (prompt: string): Promise<Uint8Array | null> => {
  try {
    const response = await fetch(
      `${CLOUDFLARE_API_URL}/@cf/lykon/dreamshaper-8-lcm`,
      {
        method: "POST",
        headers: {
          Authorization: `Bearer ${API_KEY}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          prompt,
          negative_prompt: `
          blurry, low quality, rough, distorted, incorrect colors, unrelated animals, cats, birds, short-haired, short fur, smooth fur, 
          missing ribbon, wrong ear, missing ears, left ear ribbon, messy background, abstract elements, cartoonish, unrealistic lighting, 
          overexposed, underexposed, incorrect proportions, fat, overweight, thin, skeletal, aggressive, scary, sad, angry, dull, 
          wrong hair color, no bronze hair, black fur, gray fur, missing details, low contrast, muted colors, pixelated, low resolution,
          text, watermark, logo, extra limbs, extra ears, missing legs, cropped, out of frame, incomplete, wrong breed, toy dog, wolf, fox, only two ears.
          `,
          height: 512,
          width: 512,
          num_steps: 20,
          guidance: 1,
        }),
      }
    );

    if (!response.ok) {
      console.error(`Failed to generate image: ${response.statusText}`);
      return null;
    }

    const buffer = await response.arrayBuffer();
    return new Uint8Array(buffer);
  } catch (error) {
    console.error("Error generating image:", error);
    return null;
  }
};

// 画像保存
const saveImage = async (filename: string, data: Uint8Array) => {
  try {
    if (!existsSync(OUTPUT_DIRECTORY)) {
      mkdirSync(OUTPUT_DIRECTORY, { recursive: true });
    }

    const filePath = path.join(OUTPUT_DIRECTORY, filename);
    await writeFile(filePath, data);
    console.log(`Image saved to ${filePath}`);
    return filePath;
  } catch (error) {
    console.error("Error saving image:", error);
    return null;
  }
};

app.post("/generate", async (c) => {
  const { prompt } = await c.req.json();

  if (!prompt) {
    return c.json({ error: "Prompt is required" }, 400);
  }

  try {
    // 画像生成
    const resultImage = await generateImage(prompt);
    if (!resultImage) {
      return c.json({ error: "Image generation failed" }, 500);
    }

    // 画像保存
    const timestamp = Date.now();
    const filename = `generated_image_${timestamp}.png`;
    const filePath = await saveImage(filename, resultImage);

    if (!filePath) {
      return c.json({ error: "Failed to save image" }, 500);
    }

    return c.json({ message: "Image generated successfully", path: filePath });
  } catch (error) {
    console.error("Error processing request:", error);
    return c.json({ error: "Server error" }, 500);
  }
});

export default app;

Bun.serve({
  fetch: app.fetch,
  port: 8080,
});

動作確認

サーバーの起動

以下のコマンドでサーバーを起動します:

bun run index.ts

APIリクエスト

curl コマンドで画像生成をテストします:

curl -X POST http://localhost:8080/generate \
-H "Content-Type: application/json" \
-d '{ "prompt": "A cute, long-bronz-haired dachshund with a red ribbon on its right ear." }

生成画像確認

生成された画像は generated_images ディレクトリに保存されます。
初回の生成結果がこちら!

ん〜、何とも言えない出来上がりですね🤔
プロンプトには、「右耳に赤いリボンをつけた、かわいい長毛のブロンズ色のダックスフント」を期待して指定したのですが、生成された画像を見ると、片耳にリボンがついていなかったり、どうも期待通りにはいきませんでした。テキストから画像を生成する AI モデルでは、プロンプトの解釈によって出力結果が大きく異なることがあり、特に細かいディテールやスタイルの指定が難しいことがわかります。

guidance プロパティの調整

プロンプトに対する忠実度をコントロールできるプロパティ guidance を使用して、再生成を試みました。このプロパティは、数値が大きいほどプロンプトに忠実な画像を生成しようするそうです。ドキュメントには上限がないとのことなので、今回は数値を 1 から 10000 に引き上げてみました。
再生成した結果がこちらです!

うーん、再度チャレンジしたものの、やはり「長毛で片耳にリボンをつけたかわいいダックスフンド」には見えませんね 😇

モデルの利用について

ちなみに、今回使用した Cloudflare Workers AI のモデルは、レート制限 こそありますが、無料で利用できる点が魅力です。興味のある方は、ぜひ試してみてください!詳細については公式ドキュメントをご覧ください。

ちなみに今回使用したモデルはレート制限はあるものの無料で使用できる点が魅力です。
興味のある方は、ぜひ試してみてください!詳細については公式ドキュメントをご覧ください。

おわりに

今回は、Cloudflare Workers AI を使って、テキストから画像を生成する方法を試してみました。特に、dreamshaper-8-lcm モデルを活用することで、プロンプトに基づいた高品質な画像生成がどのように行えるかを確認しました。プロンプトの設定やパラメータの調整によって、生成される画像の質が大きく変わるため、試行錯誤しながら理想の出力を目指す楽しさがありました。

次回は、今回生成した画像をさらに活用するため、画像分類に挑戦してみたいと思います。具体的には、Cloudflare Workers AI の resnet-50 モデルを使用して、生成した画像をどのように分類できるか試してみます。

参考リンク

toraco株式会社のテックブログ

Discussion