Next.js、Replicate、Redis を使用して AI を活用した画像キャプション アプリを作成する
AI がより身近になるにつれて、Replicate のような企業は、機械学習モデルをプロジェクトにシームレスに統合することを容易にしました。
この記事では、ユーザーが画像をアップロードし、AI によって生成されたテキスト キャプションを受け取ることができる Web アプリケーションである CaptionAI をどのように構築したかについて説明します。私はこの Vercel テンプレートを使用してこのプロジェクトを構築しました。このプロジェクトの構築方法を説明するビデオもあります。

使用するもの
- Next.js 13 (フロントエンドとバックエンド)
- Upstash Redis (レート制限)
- レプリケート (機械学習 API)
- Tailwind CSS (スタイリング)
- Vercel (展開)
必要なもの
- データベースを作成するための Upstash アカウント
- 機械学習 API にアクセスするためのレプリケート アカウント
Upstash Redis のセットアップ
Upstash アカウントを作成してログインしたら、[Redis] タブに移動してデータベースを作成します。


データベースを作成したら、「詳細」タブに移動します。 REST API セクションが見つかるまで下にスクロールし、.env ボタンを選択します。コンテンツをコピーし、安全な場所に保存します。

レプリケートの設定
レプリケート アカウントを作成してログインしたら、[アカウント] タブに移動して API トークンを安全な場所に保存します。
*注意:Replicate は無料で使用できますが、しばらくするとクレジット カードの入力を求められます。価格は使用するモデルによって異なります。私たちが使用しているモデル、salesforce/blip の実行コストは約 0.00042 ドルです。

プロジェクトのセットアップ
プロジェクトを最初から作成するのではなく、GitHub からリポジトリのクローンを作成できます。
リポジトリのクローンを作成したら、.env ファイルを作成します。 .example.env ファイルの情報を .env ファイルにコピーします。情報をコピーしたら、上記のセクションで保存した項目を追加します。
次のようになります:
// .env
REPLICATE_API_KEY="your_replicate_api_key_from_above"
// Optional, if you're doing rate limiting
UPSTASH_REDIS_REST_URL="your_upstash_redis_rest__url_from_above"
UPSTASH_REDIS_REST_TOKEN="your_upstash_redis_rest__token_from_above" この情報を含めると、ターミナルに次のコマンドを入力してプロジェクトを実行できるようになります。
npm install npm run dev リポジトリの構造
これはプロジェクトのメイン フォルダー構造です。画像のアップロード、レート制限、BLIP ML API の実装について説明するファイルを赤で囲みました。

高レベルのデータ フロー
これは、データがどのように流れるかを示す高レベルの図です。ユーザーがアップロードした画像である入力は、アップロード コンポーネントを経由してバックエンドに送られ、BLIP ML API 処理が行われ、UI に応答テキストが表示されます。

Redis インスタンスを作成する
プロジェクト内で、必要に応じてプロジェクト全体で参照できる upstash Redis クライアントをセットアップします。
// `/utils/redis.ts`
import { Redis } from "@upstash/redis";
const redis =
!!process.env.UPSTASH_REDIS_REST_URL && !!process.env.UPSTASH_REDIS_REST_TOKEN
? new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
})
: undefined;
export default redis; このコード スニペットは、「@upstash/redis」パッケージから Redis モジュールをインポートし、新しい Redis インスタンスを作成します。インスタンスは、UPSTASH_REDIS_REST_URL と UPSTASH_REDIS_REST_TOKEN という 2 つの環境変数の存在に基づいて条件付きで作成されます。
両方の変数が定義されている場合、指定された URL とトークンを使用して新しい Redis インスタンスが作成されます。どちらかまたは両方の変数が未定義の場合、redis 変数は未定義に設定されます。最後に、アプリケーションの他の部分で使用するために、redis 変数がモジュールからエクスポートされます。
画像のアップロード
// `/pages/captions.tsx`
const uploader = Uploader({
apiKey: !!process.env.NEXT_PUBLIC_UPLOAD_API_KEY
? process.env.NEXT_PUBLIC_UPLOAD_API_KEY
: "free",
});
const options = {
maxFileCount: 1,
mimeTypes: ["image/jpeg", "image/png", "image/jpg"],
editor: { images: { crop: false } },
styles: {
colors: {
primary: "#5a5cd1", // Primary buttons & links
error: "#d23f4d", // Error messages
shade100: "#fff", // Standard text
shade200: "#fffe", // Secondary button text
shade300: "#fffd", // Secondary button text (hover)
shade400: "#fffc", // Welcome text
shade500: "#fff9", // Modal close button
shade600: "#fff7", // Border
shade700: "#fff2", // Progress indicator background
shade800: "#fff1", // File item background
shade900: "#ffff", // Various (draggable crop buttons, etc.)
},
},
onValidate: async (file: File): Promise<undefined | string> => {
let isSafe = false;
try {
isSafe = await NSFWPredictor.isSafeImg(file);
if (!isSafe) va.track("NSFW Image blocked");
} catch (error) {
console.error("NSFW predictor threw an error", error);
}
return isSafe
? undefined
: "Detected a NSFW image which is not allowed. If this was a mistake, please contact me at hosna.qasmei@gmail.com";
},
}; このコードは、アップローダー コンポーネントの構成オプションを設定します。アップローダーは Uploader() 関数を使用して作成され、オプションはオブジェクトとしてアップローダーに渡されます。
最初の構成オプションは、アップローダー サービスでの認証に使用される apiKey です。 apiKey の値は、環境変数 NEXT_PUBLIC_UPLOAD_API_KEY が設定されているかどうかに基づいて決定されます。設定されている場合は環境変数の値が使用され、それ以外の場合は値「free」が使用されます。
オプション オブジェクトには、アップローダーのさまざまなオプションが含まれています。これらには以下が含まれます:
- maxFileCount:一度にアップロードできるファイルの最大数を 1 に設定します。
- mimeTypes:アップロードされたファイルに許可される MIME タイプを「image/jpeg」、「image/png」、「image/jpg」に設定します。
- editor:画像エディターのオプションを構成します。この場合、crop を false に設定することで無効になります。
- styles:アップローダー UI のカスタム スタイルを定義します。
- onValidate:各ファイルをアップロードする前に検証するために呼び出される関数を定義します。この場合、関数は NSFWPredictor を使用して、イメージが安全に作業できるかどうかを確認します。画像が安全でない場合は、画像が許可されていないことを示すエラー メッセージが返されます。
// `/pages/captions.tsx` continued
const Home: NextPage = () => {
const [originalPhoto, setOriginalPhoto] = useState<string | null>(null);
const [caption, setCaption] = useState<string | null>(null);
const [buttonText, setButtonText] = useState("Copy");
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const copyToClipboard = () => {
navigator.clipboard.writeText(caption!);
setButtonText("Copied!"); // set the button text to "Copied!" when text is copied
setTimeout(() => {
setButtonText("Copy"); // set the button text back to "Copy" after 2 seconds
}, 2000);
};
const UploadDropZone = () => (
<UploadDropzone
uploader={uploader}
options={options}
onUpdate={(file) => {
if (file.length !== 0) {
setOriginalPhoto(file[0].fileUrl.replace("raw", "thumbnail"));
generateCaption(file[0].fileUrl.replace("raw", "thumbnail"));
}
}}
width="670px"
height="250px"
/>
);
async function generateCaption( fileUrl: string )
{
await new Promise((resolve) => setTimeout(resolve, 500));
setLoading(true);
const res = await fetch("/api/generate", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ imageUrl: fileUrl }),
});
let newCaption = await res.json();
if (res.status !== 200) {
setError(newCaption);
} else {
setCaption(newCaption);
}
setLoading(false);
}
...
useState フックで定義された状態変数がいくつかあります。
originalPhotoは、アップロードされた画像の URL を表す文字列です。captionアップロードされた画像に対して生成されたキャプションを含む文字列です。buttonTextは、コピー ボタン上のテキストを表す文字列です。loadingコンポーネントが現在データをフェッチしているかどうかを示すブール値です。errorキャプション生成プロセス中にエラーが発生した場合のエラー メッセージを含む文字列です。
このコンポーネントには copyToClipboard と呼ばれる関数があり、navigator.clipboard.writeText メソッドを使用してキャプション変数をクリップボードにコピーします。テキストがコピーされると、buttonText 変数が「Copied!」に変更されます。 2 秒間待ってから、「コピー」にリセットします。
指定されたアップローダーとオプションを使用して UploadDropzone コンポーネントのインスタンスをレンダリングする UploadDropZone と呼ばれるサブコンポーネントもあります。 onUpdate コールバックは、アップロードされた画像の URL と生成されたキャプションを使用して、originalPhoto 変数と caption 変数を更新するために使用されます。
最後に、アップロードされた画像の URL である fileUrl パラメーターを受け取る、generateCaption という名前の非同期関数があります。 fetch を使用して POST リクエストで /api/generate エンドポイントを呼び出し、fileUrl を JSON ペイロードとして渡します。次に、応答は JSON として解析され、応答が成功したかどうかに応じてキャプション変数またはエラー変数が設定されます。ロード変数も更新されて、リクエストがまだ進行中かどうかを示します。この関数には、API レート制限に達しないようにするため、setTimeout 関数を使用した 500 ミリ秒の遅延も含まれています。
レート制限
// `/pages/api/generate.ts`
import redis from "../../utils/redis";
import requestIp from "request-ip";
import { Ratelimit } from "@upstash/ratelimit";
import type { NextApiRequest, NextApiResponse } from "next";
type Data = string;
interface ExtendedNextApiRequest extends NextApiRequest {
body: {
imageUrl: string;
};
}
// Create a new ratelimiter, that allows 3 requests every 15 minutes
const ratelimit = redis
? new Ratelimit({
redis: redis,
limiter: Ratelimit.fixedWindow(5, "1440 m"),
analytics: true,
})
: undefined;
... このコードは、Next.js で API エンドポイントを作成するために必要なモジュールとタイプ、および Upstash Redis データベース クライアントと「@upstash/ratelimit」と呼ばれるレート リミッター ライブラリをインポートします。
ratelimit 定数は、Ratelimit クラスの新しいインスタンスを作成します。これにより、1440 分 (24 時間) ごとに 5 つのリクエストを許可する固定ウィンドウ レートリミッターが作成されます。 redis プロパティはパラメータとして Ratelimit コンストラクターに渡され、アプリケーションの複数のインスタンスにわたるレート制限を有効にします。 Redis が未定義の場合 (Redis データベースが構成されていない場合など)、ratelimit も未定義に設定されます。これは、Redis が利用できない場合、レート制限は適用されないことを意味します。
// `/pages/api/generate.ts` continued
export default async function handler(
req: ExtendedNextApiRequest,
res: NextApiResponse<Data>
) {
// Rate Limiter Code
if (ratelimit) {
const identifier = requestIp.getClientIp(req);
const result = await ratelimit.limit(identifier!);
res.setHeader("X-RateLimit-Limit", result.limit);
res.setHeader("X-RateLimit-Remaining", result.remaining);
if (!result.success) {
res
.status(429)
.json("Too many uploads in 1 day. Please try again after 24 hours.");
return;
}
}
... このコード ブロックは、クライアントが API に対して行うことができるリクエストのレートを制限する API ハンドラー関数の一部です。まず、レート リミッター インスタンスが利用可能かどうかを確認し、利用可能な場合は、request-ip パッケージを使用してクライアントの IP アドレスを抽出し、それを ratelimit.limit メソッドに渡します。このメソッドは、指定された時間枠内に残っているリクエストの数と、リクエストが成功したかどうかを含むオブジェクトを返します。
リクエストが成功すると、X-RateLimit-Limit ヘッダーと X-RateLimit-Remaining ヘッダーが応答に設定されます。リクエストの制限を超えた場合、429 ステータス コードとエラー メッセージがレスポンスで送信され、関数はそれ以上の実行を防ぐために早期に戻ります。
BLIP ML API
// `/pages/api/generate.ts` continued
const imageUrl = req.body.imageUrl;
let startResponse = await fetch("https://api.replicate.com/v1/predictions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Token " + process.env.REPLICATE_API_KEY,
},
body: JSON.stringify({
version:
"2e1dddc8621f72155f24cf2e0adbde548458d3cab9f00c0139eea840d0ac4746",
input: {
image: imageUrl,
task: "image_captioning",
},
}),
});
...
コードのこの部分は、リクエスト本文から imageUrl を取得し、POST リクエストを「https://api.replicate.com/v1/predictions」エンドポイントに送信し、image_captioning タスクを使用して画像キャプションを取得します。リクエストには、認証用の Replicate API キーを含む Authorization ヘッダーが含まれており、Content-Type ヘッダーは「application/json」に設定されます。 API からの応答は JSON として解析され、endpointUrl が jsonStartResponse オブジェクトから抽出されます。
使用するモデルを選択すると、モデルのバージョン番号を確認できます。

[API] タブを選択します。

赤枠で囲まれたバージョン番号が表示されるまで下にスクロールします。青枠で囲まれた部分は、使用できる入力パラメータです。 
// `/pages/api/generate.ts` continued
let jsonStartResponse = await startResponse.json();
let endpointUrl = jsonStartResponse.urls.get;
// GET request to get the status of the image restoration process & return the result when it's ready
let caption: string | null = null;
while (!caption) {
// Loop in 1s intervals until the alt text is ready
console.log("polling for result...");
let finalResponse = await fetch(endpointUrl, {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: "Token " + process.env.REPLICATE_API_KEY,
},
});
let jsonFinalResponse = await finalResponse.json();
if (jsonFinalResponse.status === "succeeded") {
caption = jsonFinalResponse.output;
} else if (jsonFinalResponse.status === "failed") {
break;
} else {
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
res.status(200).json(caption ? caption : "Failed to generate caption");
} 次に、キャプションの準備ができるまで、while ループを使用して 1 秒間隔で endpointUrl をポーリングします。このループは、同じ Authorization ヘッダーと Content-Type ヘッダーを持つ GET リクエストを endpointUrl に送信し、応答も JSON として解析されます。 jsonFinalResponse オブジェクトのステータスが「成功」の場合、キャプションは出力プロパティから抽出されます。ステータスが「失敗」の場合、ループは終了します。ステータスが「成功」でも「失敗」でもない場合、ループは再度ポーリングする前に setTimeout メソッドを使用して 1 秒待機します。
最後に、キャプションは、null でない場合はステータス コード 200 の JSON 応答として返されます。null でない場合は、「キャプションの生成に失敗しました」というメッセージを含む応答がステータス コード 200 で返されます。
結論
結論として、このプロジェクトは、画像のアップロード、レート制限、機械学習 API の統合の実装において貴重な経験を提供しました。このプロジェクトを成功裡に完了することで、私たちはこれらのテクノロジーと、将来的により高度なプロジェクトを作成するためにそれらをどのように利用できるかについてより深く理解できるようになりました。
-
Redis SETBIT –Redisの文字列の特定のインデックスにビット値を設定する方法
このチュートリアルでは、redisデータストアのキーに格納されている文字列値の指定されたインデックスにビット値を設定する方法について学習します。このために、redis SETBITを使用します コマンド。 SETBITコマンド このコマンドは、キーに格納されている文字列値の指定されたインデックスにビット値を設定します。インデックスが文字列値の長さより大きい場合、文字列は0ビットの連続したスペースと見なされます。キーが存在しない場合は、最初に作成され、空の文字列に設定されます。したがって、インデックスは常に範囲外になり、その値は0ビットになります。 キーは存在するが、キーに格納されてい
-
Upstash Redis を Cloudflare ワーカーに簡単に接続:サーバーレス URL 短縮ツールを構築する
Cloudflareは最近、Cloudflareワーカー向けのUpstash統合をリリースしました。これにより、Workers で Upstash 製品をさらに簡単に使用できるようになります。このブログ投稿では、Upstash Redis 統合を使用してサーバーレス URL 短縮ツールを構築します。 はじめに このプロジェクトを構築する手順は次のとおりです。 Upstash データベースをセットアップする Cloudflare でワーカーを作成する Cloudflare Worker を Upstash と統合する プロジェクト アルゴリズムを実装します。 アプリケーションをデプロイする