この記事はCloudflare Advent Calendar 2024 1日目の記事です🎅
Cloudflareの魅力を知ってもらえるハンズオンを作りました!所要時間は10分程度。初めての方で迷わないよう丁寧に解説していきます。Workers AI、楽しいです✨
WebブラウザだけでAI画像生成アプリを作ろう
この記事は、Cloudflare WorkersとWorkers AIを使って、AI画像生成アプリをサーバーレスで構築するハンズオンです。
ブラウザ上で直接コードを記述し、ボタン一つでデプロイできるので、手間を最小限に抑えつつ、最新のAI技術に触れられます。Cloudflareは無料枠があるので、アプリ作りもAIの利用も気軽に実践できるのが魅力です。
この記事で開発するアプリは、日本語のプロンプトを英語に翻訳してからAIモデルで画像を生成するのが特徴です。例えば、プロンプトに『かわいい猫をポップアート風に』と入力すると、AIが猫のイラストを生成します。
それでは、ステップ・バイ・ステップで一緒にAI画像生成アプリを作っていきましょう!
Cloudflare Workersとは
Cloudflare Workersは、WebアプリケーションやAPIを構築するためのサーバーレスプラットフォームです。世界中に広がるエッジネットワーク上でコードを実行するので、レスポンスの早さが特徴です。
何より、無料ではじめられるのがいいですよね!なんと1日あたり10万件のリクエストをさばけます。商用利用もOKです。
通常、Cloudflare Workersを使って開発する際にはCLI(Command Line Interface)を利用しますが、今回はCLIを使わず、Webブラウザのみで開発からデプロイまでを実現します。
Cloudflare Workers AIとは
Cloudflare Workers AIは、Cloudflareのエッジネットワークで直接AIモデルを実行できるプラットフォームです。
こちらも無料枠があるため、気兼ねなく試すことができます。
Cloudflare Workers AIではさまざまなモデルを利用できます。一覧は次に掲載されています。
例えば、Text GenerationはChatGPTのようにテキストを生成します。Text-to-Imageはテキストから画像を生成します。Image-to-TextやObject Detectionといったモデルも使えます。
ただ、いくつか試してみたところ、日本語の入力だと期待通りの結果を得るのが難しい印象でした。特に画像生成では、プロンプトを英語に翻訳してからAIモデルに渡す方法が有効です。
ハンズオンで画像生成アプリを作ろう
実際に画像生成アプリを作りながら、Cloudflare WorkersとAIモデルを使ったアプリ構築の手順を学びます。 手を動かしながら学ぶことで、サーバーレス環境でのAI実装の基礎を身に付けることができます。
必要な知識と準備
JavaScriptとHTMLの基礎知識が必要です。簡単なWebアプリの構築ができる程度の知識があれば問題ありません。
このハンズオンに参加するためには、Cloudflareのアカウントが必要です。Cloudflareアカウントの登録は無料で、クレジットカードの登録も不要です。以下の手順でアカウントを作成してください。
- Cloudflareの公式サイトにアクセスします
- 「Start free today」ボタンから、アカウント登録画面に移動します
- 必要事項を入力して登録を完了させてください
Cloudflareにログインできたら、Cloudflare Workersの利用ができる状態になっています。
ハンズオンの手順
ここからは、Webアプリを作成する手順に沿って進めていきます。まずはWorkersで画面を表示し、それから画像生成機能を追加していきます。
Webアプリの画面を作る
1. Workerを追加する
Cloudflareのダッシュボードにログインしたら、左ペインからWorkers & Pages
を選び、新しくアプリケーションを作成します。
そのまま"Hello World" Worker
を作ります。Deployしておきます。
2. index.htmlを追加する
Edit Code
を押すとエディターが表示されます。
最初の画面ではファイル一覧が隠れています。左ペインにあるファイルアイコンを押すと、ファイル一覧が表示されます。
ファイル一覧の上部にあるファイル追加アイコンを押して、index.html
を追加し、次のコードをコピペします。
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>
<!-- Step2 翻訳した英語プロンプトで画像生成-->
<!-- <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> -->
<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>
<script>
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;
}
}
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 prompt = document.getElementById("prompt");
const translatedPrompt = document.getElementById("translated-prompt");
const formData = new FormData();
// Step1 日本語プロンプトで画像生成
formData.set("prompt", prompt.value);
// Step2 翻訳した英語プロンプトで画像生成
// 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>
</html>
しかしindex.html
を追加しただけでは、画面は変化しません。workers.js
を書き換える必要があります。
3. workers.jsを更新して画面を表示する
既存のworkers.js
を次のコードに書き換えます。
workers.js を表示する
import html from './index.html'
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === "/") {
return new Response(html, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
});
}
if (request.method === "POST" && url.pathname === "/translate") {
const formData = await request.formData();
const prompt = formData.get("prompt");
return await translatePrompt(prompt, env);
}
if (request.method === "POST" && url.pathname === "/generate-image") {
const formData = await request.formData();
const prompt = formData.get("prompt");
return await generateImage(prompt, env);
}
return new Response("Not found", { status: 404 });
},
};
async function translatePrompt(prompt, env) {
const messages = [
{ role: "system", content: "If the following text is in Japanese, translate it into English phrases without additional comments. If it is already in English, reply with the text exactly as it is." },
{ 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",
},
});
}
}
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",
},
});
}
}
4. Previewで動作を確認する
エディターの右上にある回転矢印アイコンのプレビューボタンを押して、コピペしたソースコードが正しく動作するか確認してみましょう。
Uncaught Error: No such module "index.html". imported from "worker.js"
というエラーが出るかもしれません。
その場合は、index.html
ファイルを追加しているか、ファイル名に間違いがないかを確認してください。ファイル名を変更するには、ファイルのところで、Controlキーを押しながらクリックしてRename...
を選びます(Enterを押すだけでもOKです)。
ファイルを正しく更新できていれば、次のようなプレビュー画面が表示されます。
では、好きな文字を入れて、画像生成ボタンを押してみます。
すると...
画像生成に失敗しました。もう一度試してください。
というエラーが表示されます。画面下のコンソールには、画像生成エラー: TypeError: Cannot read properties of undefined (reading 'run')
と記載されています。
Cloudflare Workersのエディターはリモートデバッグ機能を使っています。コンソールに表示されるのはバックエンド(workers.js
)の情報です。
AIを使える状態にするのを忘れていました...。気を取り直して、AIを追加します。
AIを呼び出す
5. BindingsにAIを追加する
ダッシュボードに戻ってAIを追加しましょう。
エディター画面左上にある、左矢印アイコンを押すことで、エディター画面からダッシュボードに戻ることができます。ただ、ダッシュボードに戻ろうとすると、保存(Save)するかどうかを聞かれます。
エディター上で編集したコードですが、Deployしないと保存されないようです。Saveボタンを押して保存しておきます。
ダッシュボードに戻ったら、Settingsタブから、BindingsをAddします。
さらに、下の方にあるWorkers AI
を選び、変数名(Variable Name)をAI
として、右下にあるDeployボタンを押します。
ここまでできたら、右上のEdit Codeを押して、エディター画面に戻ります。
6. 日本語で画像を生成する
気を取り直して、かわいい猫
と入力して、画像生成ボタンを押します。しばらく待つと、猫が表示されました!
次に飛んでる猫
と入力して画像生成してると...同じ画像が表示されてしまいます。
代わりに英語でflying cat
と入力すると、飛んでる猫が表示されました!
画像生成AIはかろうじて日本語を理解しているものの、細かい指示は反映できないことがわかりました。
7. ソースコードを修正する
画像生成AIを使いこなすためには、日本語ではなく、英語で指示するのがよさそうです。
そこで、テキスト生成AIを使って日本語を英語に翻訳してから、その英語で画像を生成することにします。
index.html
にあるコメントを外していきます。必ず2つとも外してください。
1つ目はHTMLのコメントアウトです。BeforeをAfterに変えます。
Before:
<!-- Step2 翻訳した英語プロンプトで画像生成-->
<!-- <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> -->
After:
<!-- Step2 翻訳した英語プロンプトで画像生成-->
<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>
2つ目はJavaScriptのコメントアウトです。BeforeをAfterに変えます。
Before:
// Step1 日本語プロンプトで画像生成
formData.set("prompt", prompt.value);
// Step2 翻訳した英語プロンプトで画像生成
// formData.set("prompt", translatedPrompt.value);
After:
// Step1 日本語プロンプトで画像生成
// formData.set("prompt", prompt.value);
// Step2 翻訳した英語プロンプトで画像生成
formData.set("prompt", translatedPrompt.value);
8. 日本語を英語に翻訳し、英語で画像を生成する
ソースコードの修正ができたら、先ほどと同じように画面右上のプレビューボタンを押してみます。すると、英語に翻訳ボタンと、翻訳結果テキストエリアが表示されました。
飛んでる猫
を翻訳するとFlying cat
が表示され、さらに画像を生成すると飛んでる猫が表示されました!(羽が生えてる!?)
9. system
プロンプトを工夫して多彩な画像を出力する
日本語から英語に翻訳するために生成AIを使ってきました。ただ、これでは既存の翻訳APIを使うのと何ら変わりはありません。
せっかくなので生成AIのパワーを引き出して、より多彩な画像を出力させるために、翻訳以上の仕事をさせてみます。
workers.js
のtranslatePrompt()
で使っているsystem
プロンプトを、次のように変更してみます。
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 },
];
修正前のプロンプト
- If the following text is in Japanese, translate it into English phrases without additional comments. If it is already in English, reply with the text exactly as it is.
- (意味)次のテキストが日本語の場合は、それを英語のフレーズに翻訳してください。ただし、追加のコメントは記載しないでください。すでに英語の場合は、そのままのテキストを正確に返信してください。
修正後のプロンプト
- 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.
- (意味) 以下の日本語テキストを、画像生成用の簡潔で説明的な英語プロンプトに翻訳してください。指定されたアートスタイル(例:油絵、水彩画、ポップアート、またはその他指定されたスタイル)に従いつつ、漫画やコミック風の要素は明示的に排除してください。画像に必要な主題、動作、視覚的な詳細を明確に記述することに集中し、元の内容やスタイルを超える要素を追加しないでください。出力は、引用符やコメント、追加のフォーマットを含まず、1つの英語文として提供してください
この修正で、画像生成用のためにより詳細な翻訳が出力されるようになりました。芸術スタイルも反映されます。
では早速試してみましょう。変更を反映するため、画面右上のプレビューボタンを押しておきます。
そして「かわいい猫をポップアート風に」と入力して[英語を翻訳]ボタンを押すと、充実した翻訳結果が表示されます。例えば、私の環境では次のように表示されました。
- 修正前の翻訳結果
- Cute cat in a pop art style
- 修正後の翻訳結果
- A colorful, exaggerated cat with bold, graphic lines, large eyes, and a bright pink nose, set against a background of swirling confetti and bold, contrasting shapes, à la Andy Warhol.
[画像を生成]ボタンを押すと、鮮やかな絵が生成されました🎉
もし生成された画像が気に入らない場合、[英語に翻訳]ボタンを押してやり直すか、翻訳結果の英語を直接修正してから、[画像を生成]ボタンを押してみてください。
これでハンズオンは終了です。ぜひ遊び倒してくださいね。
ただ、今回作ったアプリには以下のようなセキュリティの課題が残っています:
- ボットによる不正利用のリスク
- APIが誰でも利用可能な状態で、不正アクセスの懸念がある
全世界にデプロイをするのは現段階では控えましょう。
デプロイを取り消すには、このプロジェクトのSettingsタブを開き、Domains & Routes のType workers.dev
を Disable
にしてください。
以下は全世界に公開されている状態です。ここでDisableを押します。
すると次のように確認されます。[Disable]ボタンを押せば非公開になります。
不正なアクセスによって無料枠が削られるのは腹が立ちますよね。詳しくはまとめに記載しますが、これから2回に渡ってセキュリティを強化していきます。ぜひご覧ください。
このハンズオンで学べる技術
お疲れさまでした!
この章では、ハンズオンで使った技術について詳しく解説していきます。
WorkerでHTMLを使う方法
Cloudflare Workersでは、静的なHTMLを含めたコンテンツを返すことができます。今回のハンズオンでは、index.html
ファイルを用意し、Workers内からそのHTMLを返すことでWebページを表示しました。
ポイント
-
ファイルのインポート:
import html from './index.html';
という形でHTMLファイルを直接インポートできます -
HTMLの返却: HTMLコンテンツを
Response
として返すことで、クライアントに画面を表示できます
import html from './index.html'
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
if (url.pathname === "/") {
return new Response(html, {
headers: {
"content-type": "text/html;charset=UTF-8",
},
});
}
...
この手法を使うと、Workers内でフロントエンド部分を手軽に管理でき、シンプルなWebアプリケーションを容易にホスティングすることが可能です。
ただ、静的なコンテンツを配信するのであれば、Cloudflare WorkersのStatic AssetsやCloudflare Pagesの方がお得です。一切コストがかかりません!
この記事ではWebブラウザーだけでWebアプリを作るお手軽な方法をご紹介しました。ただ、Static Assetsが使えないなどの制限も多いため、ローカル開発環境を構築し、CLI(Command Line Interface)を使った開発に取り組むのが王道です。次の記事で、その具体的な方法をご紹介します。
Text-to-ImageのAIモデルで画像を生成する
Text-to-Imageモデルを使うと、テキストプロンプト(指示)に基づいてAIが画像を生成します。今回はCloudflare Workers AIのflux-1-schnellモデルを使って画像を生成しました。
ポイント
- BindingsにWorkers AIを追加: 忘れがちなので気をつけましょう
- プロンプトの準備: プロンプトに具体的な内容を含めると、AIがより適した画像を生成できます。英語のプロンプトを用意するのが精度向上のコツです
- 画像の返却: 画像生成AIごとに出力する画像の形式が異なります。Workers AIの各モデルの説明にサンプルコードがあるので、それを参考にしてください。
flux-1-schnellではJPEGで出力します。
一方、stable-diffusion-xl-base-1.0ではPNGで出力します。
Text GenerationのAIモデルで日本語を英語に翻訳する
Text Generationモデルは、指定したプロンプトに応じてテキストを生成します。今回は、llama-3.2-3b-instructモデルを使い、日本語のプロンプトを英語に翻訳しました。
ポイント
- BindingsにWorkers AIを追加: ハンズオンでは追加し忘れて失敗しました。忘れがちなので気をつけましょう
-
プロンプトを英語で書く:
llama-3.2-3b-instruct
はマルチリンガルに対応したモデルなのですが、日本語は苦手なようです。「日本語なら英語に翻訳し、英語ならそのまま返す」という指示を英語にして設定しています
async function translatePrompt(prompt, env) {
const messages = [
{ role: "system", content: "If the following text is in Japanese, translate it into English phrases without additional comments. If it is already in English, reply with the text exactly as it is." },
{ role: "user", content: prompt }
];
try {
const stream = await env.AI.run("@cf/meta/llama-3.2-3b-instruct", { messages, stream: true });
...
Event Streamで送信されたテキストを順次表示する
Server-Sent Events(SSE) を使用すると、サーバーからクライアントへリアルタイムのデータ送信が可能です[^es]。
ポイント
-
SSEの設定:
env.AI.run
の呼び出しパラメーターでstream: true
を設定し、text/event-stream
ヘッダーを使ってレスポンスを返すことで、サーバーからクライアントへデータがリアルタイムに流れます -
フロントエンドでのEvent Stream処理: フロントエンドでは
handleEventStream()
関数でデータストリームを読み込み、データが届くたびに表示を更新します
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;
textarea.scrollTop = textarea.scrollHeight;
} catch (error) {
// JSONデータが壊れている場合は無視する
}
}
});
}
}
まとめ
今回のハンズオンを通じて、Cloudflare WorkersとCloudflare Workers AIの強力な機能を活用し、サーバーレスでAIを駆使したWebアプリケーションを構築しました。
- Cloudflare Workersを使ってサーバーレスで簡単にWebアプリケーションを構築し、HTMLファイルを提供する方法を学びました
- Text-to-ImageとText Generationモデルの使用により、日本語から英語に翻訳し、画像を生成する仕組みを作成しました
- SSEを使用して、翻訳の結果をリアルタイムで表示する技術も紹介しました
次の記事では、セキュリティを強化するため、既存のツールやパッケージを簡単に導入できるCLIベースの開発環境に移行します。さらにその次の記事では、次の技術を活用しながらセキュリティ対策を実装していきます:
- Cloudflare Turnstile によるボット対策1
- JWT(JSON Web Token) によるセッション管理
- Originヘッダーなどの検証 によるCSRF(Cross Site Request Forgery)対策2
- CSP(Content Security Policy) や適切なサニタイズによるXSS(Cross Site Scripting)攻撃防御
Cloudflare Workers AIは、AIを活用したWebアプリの構築を手軽にし、アイデアをすばやく実現するのに最適なプラットフォームです。ぜひ、このハンズオンを通じて、次の段階のセキュリティ強化やカスタムAIアプリケーション開発に挑戦してみてください!