3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CloudflareAdvent Calendar 2024

Day 2

CLIでサクサク開発!Cloudflare Workersで作るAI画像生成アプリ

Last updated at Posted at 2024-12-01

この記事はCloudflare Advent Calendar 2024 2日目の記事です🎅

Cloudflareの魅力を知ってもらえるハンズオンを作りました!初めての方で迷わないよう丁寧に解説していきます。Workers AI、楽しいです✨

1. はじめに

この記事は、Cloudflare WorkersWorkers AIを使って、AI画像生成アプリをサーバーレスで構築するハンズオンです。

1.1 AI画像生成アプリの完成イメージ

日本語プロンプトを英語に翻訳し、英語を使って高品質なAI画像を生成するアプリです。

生成された「かわいい猫」の画像

1.2 ハンズオンで使用する技術

  • Cloudflare Workers
    サーバーを持たずにコードを直接エッジネットワーク上で実行できるサーバーレスプラットフォームです。グローバルなエッジロケーションからリクエストを処理するため、高速でスケーラブルなアプリケーションを構築できます

  • Workers AI
    最新のAIモデルを簡単に統合できるCloudflareのサービスです。テキスト生成・画像生成だけでなく、画像からのオブジェクト抽出や音声処理など様々なAIを簡単に使えます。無料枠があるので気軽に試せます

1.3 ハンズオンの特徴

1. 無料で試せる

Cloudflare WorkersやWorkers AIの無料枠を利用して、コストをかけずに実践的なアプリケーションを構築できます。

2. CLIを使ってサクサク開発

Cloudflareが提供するCLIを活用して、効率的に開発を進められます。

Cloudflare Workersはブラウザーだけで開発することもできますが、使える機能に限りがあります。CLIを使えば、静的コンテンツを無料で配信できるStatic Assetsも使えるようになります。

3. AI技術の基礎と応用を学べる

AIモデルを単体で使う方法だけでなく、英語に翻訳してから画像を生成する、といったAIを組み合わせて使う方法も体験できます。

1.4 ハンズオンのコード

この記事で紹介するソースコードは、GitHubリポジトリにも公開しています。ソースコードをまとめて確認したい場合は、以下のリンクにアクセスしてください。

2. Cloudflare WorkersでHello World

まずはCloudflare Workersを使った開発プロセスを学ぶため、Hello World!と表示する単純なWebアプリを作っていきます。

以下の手順に従って、CLIでWorkersを作っていきます。

2.1 事前準備

  1. Cloudflareのアカウントを作成します

    • クレジットカードは不要です
  2. Node.jsをインストールします

    • Node.jsは頻繁にアップデートされています。Node.jsのバージョン管理ツール(Voltanvmなど)を使うと、バージョンを指定して実行できるようになります

2.2 新しいWorkerプロジェクトを作る

ターミナルで次のコマンドを実行します。
プロジェクト名は好きなものを指定してください。この記事ではai-image-handsonとします。

npm create cloudflare@latest -- ai-image-handson

--はコマンドと引数を分ける区切りです。
このコマンドはcreate-cloudflareを実行します。その引数が--の後のai-image-handsonです。

今回は最もシンプルなWorkerを作ります。次の選択肢を選んでください。

  • What would you like to start with?
    • Hello World example
  • Which template would you like to use?
    • Hello World Worker
  • Which language do you want to use?
    • JavaScript
  • Do you want to use git for version control?
    • Yes
  • Do you want to deploy your application?
    • No

これで新しいプロジェクトを作成できました。プロジェクトのディレクトリーに移動しておきます。

cd ai-image-handson

2.3 ローカルで動作を確認する

次のコマンドでローカルの開発用サーバーを起動します。

npx wrangler dev

npxを使うと、まだインストールされていないパッケージをその場で実行できます。
パッケージをグローバルにインストールしないで実行するので、システムを汚さずに済みます。

ターミナルに表示されたURLhttp://localhost:8787をWebブラウザーで開くと、Hello World!と表示されているのが確認できます。

2.4 コードを修正する

開発用サーバーで実行されているのはsrcディレクトリーにあるindex.jsです。

src/index.js
export default {
	async fetch(request, env, ctx) {
		return new Response('Hello World!');
	},
};

表示される文字列をHello Worker!に変更してみましょう。

src/index.js
export default {
	async fetch(request, env, ctx) {
-		return new Response('Hello World!');
+		return new Response('Hello Worker!');
	},
};

Webブラウザーをリロードすると、文字列がHello Worker!に変化しました。

2.5 デプロイする

ローカルで動作を確認したら、https://ai-image-handson.<あなたのサブドメイン>.workers.devにデプロイします。

ターミナルで次のコマンドを実行します。

npx wrangler deply

なお、デプロイはCLIからだけでなく、Cloudflare Dashboardからも管理できます。

3. AI画像生成アプリを作る

Workers AIを使って、AI画像生成アプリを作っていきましょう。

翻訳機能、画像生成機能、の順番で実装します。

3.1 利用するモデルを選ぶ

Workers AIは様々なモデルを提供しています。

AIの種類(生成する情報)によって、価格が異なります1。日次でリセットされる無料枠があるので、AIの機能を気軽に試すことができます。

今回のAI画像生成アプリでは、次の2つのモデルを使います。

  • テキスト生成AI(翻訳)
    • llama-3.2-3b-instruct
    • マルチリンガルなモデルです。簡単な日本語であれば問題なく英語に翻訳できます(ただ、日本語だけで会話を続けるのは難しかったです)
  • 画像生成AI
    • flux-1-schnell
    • 高品質の2画像を高速に3生成します。出力した画像は商用利用可能です(Hugging Face
    • なお、執筆時点(2024年11月)では同じプロンプトに対して常に同じ画像が生成されます(他の画像生成AIでは同じプロンプトでも異なる画像が生成されます)。Seed値の指定はできません

3.2 翻訳の画面を作る

まずは翻訳機能の画面から作っていきます。

3.2.1 フロントエンドのindex.htmlを作成する

ルートディレクトリーにpublicディレクトリーを作成します。その配下にindex.htmlファイルを追加して、以下をそれぞれ作成します。

  • 日本語を入力するtextarea
  • 翻訳するbutton
  • 翻訳結果を表示するtextarea

なお、見栄えをよくするためnew.cssを使って画面スタイルを整えています4

`public/index.html`を展開する
public/index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>AI画像生成ハンズオン</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@exampledev/new.css@1.1.3/new.min.css">
</head>

<body>
  <h3>何をどんな風に描きたいですか?</h3>
  <p>多彩な表現を楽しもう!ポップアート風、油絵風、水彩画風、サイバーパンク風など</p>
  <textarea id="prompt" placeholder="例: かわいい猫のイラスト" style="width: 100%"></textarea>
  <button id="translate-btn" onclick="translateText()">
    英語に翻訳
  </button>
  <p id="translate-error-message" style="color: red;"></p>
  <h3 for="translated-prompt">翻訳結果</h3>
  <textarea id="translated-prompt" placeholder="ここに英語の翻訳結果が表示されます"
    style="width: 100%; field-sizing: content;"></textarea>
  <script></script>
</body>

</html>

3.2.2 バックエンドのindex.jsを修正する

src/index.jsは以下のコードですべて上書きし、Static Assetsを返却するように変更します。

`src/index.js`を展開する
src/index.js
export default {
  async fetch(request, env, ctx) {
    return env.ASSETS.fetch(request);
  },
};

3.2.3 設定wrangler.tomlでStatic Assetsを有効にする

Cloudflare Workersでは静的コンテンツ(HTML、CSS、JavaScriptなど)を無料で配信できます。この機能は非常に便利ですが、ブラウザーベースの開発環境ではwrangler.tomlの設定ができないため利用できません。

CLIを使うことで、wrangler.tomlを簡単に設定し、Static Assetsの無料配信機能を活用することが可能です。これにより、アプリの構築が効率化されるだけでなく、ランニングコストを抑えた運用が可能になります。

Static Assetsを有効にするため、wrangler.tomlに以下を追加します。

wrangler.toml
# 追加
assets = { directory = "./public/",  binding = "ASSETS" }

開発用サーバーを起動して、http://localhost:8787にアクセスし、画面が変わっているか確認しましょう。

翻訳機能を追加

まだフロントエンドから翻訳APIを呼び出す機能を実装していないため、翻訳ボタンをしてもエラーが表示されるだけです。

3.3 翻訳機能を作る

テキスト生成AIを使って、日本語を英語に翻訳します。

ただ単に翻訳するのではなく、画像生成用にアートスタイルの追加もさせています。

3.3.1 フロントエンドから翻訳APIを呼び出す

フロントエンドのロジックをpublic/script.jsに実装していきます。

/publicフォルダーの配下にscript.jsファイルを追加して、翻訳APIを呼び出します。

`public/script.js`を展開する
public/script.js
async function handleEventStream(response, textarea) {
  const reader = response.body.getReader();
  const decoder = new TextDecoder("utf-8");
  let text = "";

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;

    const chunk = decoder.decode(value, { stream: true });
    chunk.split("\n").forEach((line) => {
      if (line.startsWith("data: ")) {
        const data = line.replace("data: ", "");
        if (data === "[DONE]") return;
        try {
          const jsonData = JSON.parse(data);
          text += jsonData.response;
          textarea.value = text;
        } catch (error) {
          // JSONデータが壊れている場合は無視する
        }
      }
    });
  }
}

async function translateText() {
  const button = document.getElementById("translate-btn");
  const errorMessage = document.getElementById("translate-error-message");
  const promptInput = document.getElementById("prompt");

  button.disabled = true;
  errorMessage.style.display = "none";
  const formData = new FormData();
  formData.set("prompt", promptInput.value);

  try {
    const response = await fetch(`/translate`, {
      method: "POST",
      body: formData,
    });

    if (!response.ok) {
      throw new Error(`エラー ${response.status} ${response.statusText}`);
    }

    const translatedPrompt = document.getElementById("translated-prompt");
    await handleEventStream(response, translatedPrompt);
  } catch (error) {
    console.error("エラー: ", error);
    errorMessage.textContent = "翻訳に失敗しました。もう一度試してください。";
    errorMessage.style.display = "block";
  } finally {
    button.disabled = false;
  }
}

3.3.2 index.htmlからscript.jsを呼び出す

public/index.html<head>タグ内に、public/script.jsを読み込むコードを追加します。

public/index.html
<head>
  ...
  <!-- 追加 -->
  <script src="./script.js" defer></script>
</head>

3.3.3 Workers AIを有効にして翻訳APIを実装する

次にwrangler.tomlファイルを変更して、Workers AIを有効にします。2行分のコメントを外します。

wrangler.toml
[ai]
binding = "AI"

そしてバックエンドのsrc/index.jsに翻訳APIを実装します。

`src/index.js`を展開する
src/index.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    ...
    // 翻訳APIの呼び出しを追加
    if (request.method === "POST" && url.pathname === "/translate") {
      const formData = await request.formData();
      const prompt = formData.get("prompt");
      return await translatePrompt(prompt, env);
    }
    return env.ASSETS.fetch(request);
  },
};

async function translatePrompt(prompt, env) {
  const messages = [
    {
      role: "system",
      content: `Translate the following Japanese text into a concise and descriptive English prompt for image generation. Ensure the prompt adheres to the specified artistic style (e.g., oil painting, watercolor, pop art, or other specified), but explicitly exclude any cartoon or comic-style elements. Focus on clearly describing essential subjects, actions, and visual details for the image without introducing elements beyond the original content or style. Output the result as a single English sentence without quotation marks, comments, or additional formatting.`
    },
    { role: "user", content: prompt },
  ];

  try {
    const stream = await env.AI.run("@cf/meta/llama-3.2-3b-instruct", {
      messages,
      stream: true,
    });
    return new Response(stream, {
      headers: { "Content-Type": "text/event-stream" },
    });
  } catch (error) {
    console.error("翻訳エラー:", error);
    return new Response(JSON.stringify({ error: "翻訳に失敗しました。" }), {
      status: 500,
      headers: {
        "Content-Type": "application/json",
      },
    });
  }
}

このコードのポイント:

  • messages配列でAIに渡す指示を定義しています
    • systemロールで翻訳タスクの詳細を指示しています
    • userロールで実際に翻訳したい日本語のテキストを渡します
  • env.AI.runメソッドで指定したAIモデルを呼び出し、stream: trueでレスポンスをストリーミングします

開発用サーバーを起動すると、Workers AIが有効になったことが分かります。

npx wrangler dev

 ⛅️ wrangler 3.90.0
-------------------

Your worker has access to the following bindings:
- AI:
  - Name: AI
▲ [WARNING] Using Workers AI always accesses your Cloudflare account in order to run AI models, and so will incur usage charges even in local development.

http://localhost:8787にアクセスし、例えば最初のテキストエリアに「かわいい猫」と入力して「英語に翻訳」ボタンを押してみましょう。翻訳結果が表示されます。

翻訳結果を表示

なお、ローカルで動作させる場合、翻訳結果がまとめて表示され、ストリーミング表示になっていないように見える場合があります。デプロイすると正しく動作します。

3.4 画像生成機能を作る

画像生成AIを使って、翻訳した英語から画像を生成します。

3.4.1 画面を作成する

public/index.htmlに画像生成ボタンと生成された画像の表示領域を追加します。

`public/index.html`を展開する
public/index.html
  <textarea id="translated-prompt" placeholder="ここに英語の翻訳結果が表示されます"
    style="width: 100%; field-sizing: content;"></textarea>
  <!-- 画像生成ボタンを追加 -->
  <button id="gen-image-btn" onclick="generateImage()">
    画像を生成
  </button>
  <p id="gen-image-error-message" class="error-message" style="color: red;"></p>
  <div id="image-container" style="margin-top: 1rem;">
    <img id="generated-image" src="" alt="Generated Image" style="display: none;" />
  </div>
</body>

3.4.2 フロントエンドから画像生成APIを呼び出す

フロントエンドのpublic/script.jsに以下のコードを追加して、画像生成APIを呼び出します。

`public/script.js`を展開する
src/index.html
  <script>
    ...
    async function generateImage() {
      const button = document.getElementById("gen-image-btn");
      button.disabled = true;
      const errorMessage = document.getElementById("gen-image-error-message");
      errorMessage.style.display = "none";
      const translatedPrompt = document.getElementById("translated-prompt");
      const formData = new FormData();
      formData.set("prompt", translatedPrompt.value);

      try {
        const response = await fetch(`/generate-image`, {
          method: "POST",
          body: formData,
        });

        if (!response.ok) {
          throw new Error(`エラー ${response.status} ${response.statusText}`);
        }

        const imageBlob = await response.blob();
        const imageUrl = URL.createObjectURL(imageBlob);
        const img = document.getElementById("generated-image");
        img.src = imageUrl;
        img.style.display = "block";
      } catch (error) {
        console.error("エラー: ", error);
        errorMessage.textContent =
          "画像生成に失敗しました。もう一度試すか、プロンプトを変えてください。";
        errorMessage.style.display = "block";
      } finally {
        button.disabled = false;
      }
    }
  </script>
</body>

3.4.3 画像生成APIを実装する

src/index.jsに画像生成APIを追加します。

`src/index.js`を展開する
src/index.js
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    ...
    // 画像生成APIの呼び出しを追加
    if (request.method === "POST" && url.pathname === "/generate-image") {
      const formData = await request.formData();
      const prompt = formData.get("prompt");
      return await generateImage(prompt, env);
    }
    return env.ASSETS.fetch(request);
  },
};

async function generateImage(prompt, env) {
  const inputs = { prompt };

  try {
    const response = await env.AI.run(
      "@cf/black-forest-labs/flux-1-schnell",
      inputs
    );
    const binaryString = atob(response.image);
    const img = Uint8Array.from(binaryString, (m) => m.codePointAt(0));
    return new Response(img, {
      headers: {
        'Content-Type': 'image/jpeg',
      },
    });
  } catch (error) {
    console.error("画像生成エラー:", error);
    return new Response(JSON.stringify({ error: "画像生成に失敗しました。" }), {
      status: 500,
      headers: {
        "Content-Type": "application/json",
      },
    });
  }
}

開発用サーバーを起動して、http://localhost:8787にアクセスし、日本語から英語に翻訳した後、画像を生成ボタンを押しましょう。少し待つと生成された画像が表示されます。

生成された「かわいい猫」の画像

執筆時点(2024年11月)では、画像生成のプロンプトが同じだと、常に同じ画像が生成されます。同じ題材で複数の画像を楽しみたい場合は、もう一度[英語に翻訳]を押して別のプロンプトを生成してから、[画像を生成]を実行してください。

3.5 デプロイする前に

これでAI画像生成アプリは完成です。デプロイすれば全世界から使えるようになります。

しかし、このアプリにはセキュリティの課題が残っています。全世界へ公開する前に、次のようなセキュリティ対策が必要です。

  • ボット対策
    • Cloudflare Turnstileの導入
    • JWT(JSON Web Token)によるセッション管理
  • CSRF(Cross Site Request Forgery)対策
    • OriginヘッダーとFetch Metadataによる検証
  • XSS(Cross Site Scripting)対策5
    • CSP(Content Security Policy) やサニタイズによるXSS攻撃防御

これらの対策により、不正なアクセスからアプリケーションを守ることができます。次回の記事で詳しく解説します。

4. まとめ

この記事を通じて、Cloudflare WorkersとWorkers AIを用いたAI画像生成アプリの構築を体験しました。このハンズオンで学べることを以下にまとめます:

  1. Cloudflare Workersを使った簡単で効率的な開発・デプロイ
    サーバー不要で、ローカルのコードを数秒でデプロイし、即座に世界に公開できます。CLIを使った開発ならStatic Assetsを使って静的コンテンツを無料で配信できます

  2. Workers AIを活用した多様なAIモデルの利用
    翻訳や画像生成をはじめとする、多彩なAI技術を簡単に試し、アプリケーションに統合できます

  3. AIを利用するアプリケーションの構築体験
    翻訳と画像生成を連携させたアプリケーションを設計・実装する流れを体験し、AI技術の応用方法を学べます

次回の記事では、セキュリティを強化してより安全で堅牢なアプリに仕上げていきます。ぜひご覧ください!

参考情報

  1. https://blog.cloudflare.com/workers-ai-bigger-better-faster/#new-workers-ai-pricing ではWorkers AIの料金モデルが10月から変更になると記載されています。ただ、執筆時点(2024年11月)では古いモデルのままのようです。

  2. FLUXのパラメーターにnegative_promptありません

  3. FLUXのパラメーターの推論ステップ数num_stepsはデフォルトが4、最大が8です。Workers AIの一日当たりの無料枠が250ステップなので、デフォルトで62枚までの画像を生成できます

  4. https://zenn.dev/yusukebe/articles/59532688053828 を参考にさせていただきました!

  5. 今回作成したAI画像生成アプリでは、ユーザーの入力データを画面に表示しなおす機会がありません。XSS攻撃は難しいですが、よくある攻撃なので紹介しています。

3
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?