OpenAI、Upstash、Next.js を使用して AI を活用したストーリー ジェネレーターを構築する
このブログ投稿では、続行する前にいくつかの仮定を立てますが、次のことが理想的です。
- Redis および QStash インスタンスが作成されている Upstash アカウント
- API キーにアクセスできる OpenAI アカウント
- ストーリー ジェネレーター機能を作成する Next.js プロジェクト
- プロジェクトをデプロイする Vercel アカウント
はじめに
AIを使用して独自のストーリーを生成したいと思ったことはありますか? OpenAI のコンプリーション API、Upstash の QStash および Redis を使用すると、自然言語処理を使用して独自のカスタム ストーリーを作成することがこれまでより簡単になりました。このチュートリアルでは、これらのツールを設定して使用して、ユニークで魅力的なストーリーを生成するプロセスを順を追って説明します。

アプリの画像をもっと見る:
- ストーリー フォームを作成する
- ストーリー状態の生成
- 生成されたストーリーが表示される
アーキテクチャ
コードを確認することで、アプリの設定方法を十分に理解できると思いますが、もう少し高いレベルの概要として、以下の画像に、アプリケーション フローの一部とそれらの通信方法を示します。

プロジェクトのセットアップ
まず、Next.js プロジェクトを作成します。これを行うには、次のコマンドを実行して、TypeScript で新しい Next.js プロジェクトを作成します。 Next.js をセットアップする手順については、こちらをご覧ください。
このチュートリアルのために、Tailwind CSS (フォームとタイポグラフィーも) もインストールされていますが、これは完全にオプションであり、フロントエンド フォームのスタイル設定のみに使用されます。
次に、次の方法で Upstash の QStash ライブラリと Redis ライブラリをインストールします。
npm install @upstash/qstash
npm install @upstash/redis
次に、.env.local を作成します。 ファイルを作成し、次のキー (および関連する場所の値) を入力します。
SITE_URL=https://your-project-url.vercel.app
OPENAI_API_KEY=
QSTASH_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN= プロジェクトを作成し、基本的な Next.js プロジェクトをデプロイすると、QStash トークンと Redis トークンは Upstash コンソールで、OpenAI API キーはここで、サイト URL は Vercel ダッシュボードで確認できます。
フロントエンドのセットアップ
次にストーリープロンプトを入力するためのページとフォームを作成します。プロンプト用のテキスト フィールドと送信ボタンが必要です。
ストーリーの作成
ファイル:pages/index.tsx
import { RefObject, useRef, useState } from "react";
import Head from "next/head";
import useInterval from "../hooks/useInterval";
export default function Home() {
const [generating, setGenerating] = useState<boolean>(false);
const [messageId, setMessageId] = useState<string | null>(null);
const [story, setStory] = useState<string[]>([]);
const themeRef: RefObject<HTMLInputElement> = useRef(null);
const characterRef: RefObject<HTMLInputElement> = useRef(null);
const moralRef: RefObject<HTMLInputElement> = useRef(null);
useInterval(
async () => {
await fetch(`/api/poll?id=${messageId}`)
.then((res: any) => res.json())
.then((data: any) => {
if (!data.choices) {
return;
}
setGenerating(false);
setMessageId(null);
setStory(data.choices[0].text.split("\n\n"));
})
.catch((err: any) => console.error(err));
},
messageId ? 1000 : null,
);
async function generateStory(event: any) {
event.preventDefault();
setGenerating(true);
await fetch("/api/create", {
method: "POST",
body: JSON.stringify({
theme: themeRef.current?.value,
character: characterRef.current?.value,
moral: moralRef.current?.value,
}),
headers: { "Content-Type": "application/json" },
})
.then((res: any) => res.json())
.then((data: any) => setMessageId(data.id))
.catch((err: any) => console.error(err));
}
return (
<>
<Head>
<title>StoryTime</title>
<meta
name="description"
content="A simple Next.js application which allows you to create stories using AI."
/>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main>
<div className="my-16 flex flex-col items-center justify-center md:my-32">
<h1 className="text-5xl font-black">StoryTime</h1>
{story.length > 0 && (
<div className="mx-auto mt-10 max-w-3xl">
<div className="prose lg:prose-xl w-full">
{story.map((paragraph: string, index: number) => (
<p key={index}>{paragraph}</p>
))}
</div>
<div className="text-center">
<button
type="button"
onClick={() => setStory([])}
className="mt-6 inline-flex items-center rounded-full border border-transparent bg-gray-900 px-6 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2"
>
Start Over
</button>
</div>
</div>
)}
{story.length == 0 && (
<form
onSubmit={generateStory}
className="mt-10 flex w-full max-w-lg flex-col items-center"
>
<div className="w-full space-y-4">
<div>
<label htmlFor="theme" className="text-sm font-semibold">
My story is about
</label>
<input
name="theme"
id="theme"
type="text"
className="mt-0.5 block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500"
placeholder="two friends going on an adventure"
ref={themeRef}
required
/>
</div>
<div>
<label htmlFor="character" className="text-sm font-semibold">
My main character is
</label>
<input
name="character"
id="character"
type="text"
className="mt-0.5 block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500"
placeholder="a dog named Spot"
ref={characterRef}
required
/>
</div>
<div>
<label htmlFor="moral" className="text-sm font-semibold">
The moral of my story is
</label>
<input
name="moral"
id="moral"
type="text"
className="mt-0.5 block w-full rounded-md border-gray-300 shadow-sm focus:border-gray-500 focus:ring-gray-500"
placeholder="to always be kind"
ref={moralRef}
required
/>
</div>
</div>
<button
type="submit"
disabled={generating}
className="mt-6 inline-flex items-center rounded-full border border-transparent bg-gray-900 px-6 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2 disabled:opacity-50"
>
{generating ? "Generating..." : "Generate"}
</button>
</form>
)}
</div>
</main>
</>
);
}
このファイルは、ユーザーがストーリーのテーマ、登場人物、教訓を入力できるフォームを表示する React コンポーネントを定義します。フォームが送信されると、POST が送信されます。 /api/create へのリクエスト 入力されたテーマ、性格、道徳的価値観を本体とするエンドポイント。
その後、コンポーネントはポーリング状態に入り、GET を送信します。 /api/poll へのリクエスト これにより、ポーリング対象のストーリーへのストーリー作成リクエストを追跡し、OpenAI による生成がいつ完了したかを確認できるようになります。
/api/poll からの応答が返されたとき エンドポイントに Choices プロパティが含まれている場合、ポーリング リクエストが正常に生成されたストーリーを返したことがわかっているため、コンポーネントはポーリングを停止し、ストーリー テキストを段落に分割して各段落を個別にレンダリングして表示します。
インターバルフック
ファイル:hooks/useInterval.ts
import { useEffect, useRef } from "react";
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
useEffect(() => {
if (!delay && delay !== 0) {
return;
}
const id = setInterval(() => savedCallback.current(), delay);
return () => clearInterval(id);
}, [delay]);
}
export default useInterval;
useInterval フックは useEffect を使用します。 と useRef React コンポーネントのライフサイクルとシームレスに動作する間隔とコールバック関数を管理するためのフック。また、React コンポーネント内で間隔とコールバックを管理する便利な方法を提供し、パフォーマンスを最適化し、コードベースを少し保守しやすくします。このフックの詳細については、こことここを参照してください。
API セットアップ
まず、コールバックを作成し、ファイルをポーリングして作成し、Redis と QStash ライブラリの使用法を作成します。
ストーリーの作成
ファイル:pages/api/create.ts
import type { NextApiRequest, NextApiResponse } from "next";
import qstashClient from "../../lib/qstash";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
if (req.method !== "POST") {
return res.status(400).json({
message: `Invalid request method: ${req.method}.`,
});
}
const { theme, character, moral }: any = req.body;
qstashClient
.publishJSON({
url: "https://api.openai.com/v1/completions",
method: "POST",
headers: {
Authorization: `Bearer ${process.env.QSTASH_TOKEN}`,
"Content-Type": "application/json",
"Upstash-Callback": `${process.env.SITE_URL}/api/callback`,
"Upstash-Forward-Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: {
model: "text-davinci-003",
prompt: `Write a children's story about ${theme}, which has a main character who is ${character} with the moral of the story being ${moral}.`,
max_tokens: 500,
temperature: 0.75,
},
})
.then((data: any) => {
return res.status(202).json({ id: data.messageId });
})
.catch((error: any) => {
return res.status(500).json({ message: error.message });
});
}
まずリクエストメソッドが POST であることを確認します。 そうでない場合は、ステータス コード 400 (クライアント エラーを示す) を含む応答を送信します。次に、リクエストの本文からテーマ、性格、道徳フィールドを分解します。
次に、publishJSON を呼び出します。 qstashClient のメソッド POST を送信するオブジェクト テーマ、キャラクター、道徳の値に基づいて子供の物語を生成するためのプロンプトを含む JSON 本文を含む OpenAI API へのリクエスト。また、QSTASH_TOKEN に格納されたトークンを含む認証ヘッダーなど、いくつかのヘッダーも設定します。 環境変数、および OPENAI_API_KEY を通過するための転送された認証ヘッダー これは OpenAI API リクエストと一緒に使用されます。
publishJSON の場合、リクエストのメッセージ ID を返します。 呼び出しは成功しました。これは、リクエストがいつ終了したかを確認するためのポーリングに使用されます。エラーが発生した場合、ステータス コード 500 (内部サーバー エラーを示す) と関連するエラー メッセージを含む応答が送信されます。
コールバック
ファイル:pages/api/callback.ts
import type { NextApiRequest, NextApiResponse } from "next";
import redis from "../../lib/redis";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { body }: any = req;
try {
const decoded = Buffer.from(body.body, "base64").toString("utf-8");
await redis.set(body.sourceMessageId, decoded);
return res.status(200).send(decoded);
} catch (error) {
return res.status(500).json({ error });
}
} まず、受信リクエストの本文 (base64 でエンコードされた文字列) のデコードを試みます。成功すると、最初のリクエストを QStash に送信したときに返されたものと同じキーの下で、デコードされた文字列が Redis に保存されます。
最後に、ステータス コード 200 (成功を示す) とデコードされた文字列を含む応答を送信します。エラーが発生した場合は、ステータス コード 500 (内部サーバー エラーを示す) とエラー メッセージを含む応答が返されます。
ポーリング
ファイル:pages/api/poll.ts
import type { NextApiRequest, NextApiResponse } from "next";
import redis from "../../lib/redis";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse,
) {
const { id }: any = req.query;
try {
const data = await redis.get(id);
if (!data) {
return res
.status(404)
.json({ message: "Data for supplied ID not found" });
}
return res.status(200).json(data);
} catch (error: any) {
return res.status(500).json({ message: error.message });
}
}
まず、id を構造化します。 リクエストのクエリオブジェクトから。次に、構造化された id の下で Redis に保存されているデータを取得しようとします。 データが見つからない場合は、ステータス コード 404 (要求されたリソースが見つからなかったことを示す) とその旨を示すメッセージを含む応答を送信します。
指定されたキーに属するデータが見つかった場合は、ステータス コード 200 (成功を示す) を含む応答と、見つかったデータが送信されます。エラーが発生した場合、ステータス コード 500 (内部サーバー エラーを示す) と関連するエラー メッセージを含む応答が返されます。
ライブラリ
次に、ストーリー生成プロセス内で使用される QStash クライアントと Redis クライアントを作成するための 2 つのファイルを作成します。どちらのファイルも、それぞれの外部サービスと対話するために使用されるオブジェクトをエクスポートします。
ファイル:lib/qstash.ts
import { Client } from "@upstash/qstash";
const qstashClient = new Client({
token: process.env.QSTASH_TOKEN as string,
});
export default qstashClient;
QStash クライアントは、QSTASH_TOKEN に保存されたトークンを使用して初期化されます。 環境変数。このオブジェクトは、HTTP リクエストを Upstash QStash サービスに送信するために使用できます。
ファイル:lib/redis.ts
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL as string,
token: process.env.UPSTASH_REDIS_REST_TOKEN as string,
});
export default redis;
Redis クライアントは、UPSTASH_REDIS_REST_URL に保存された URL とトークンを使用して初期化されます。 および UPSTASH_REDIS_REST_TOKEN それぞれ環境変数。このオブジェクトは、Upstash Redis REST API を通じて Redis データベースにデータを保存および取得するために使用できます。
結論
OpenAI のコンプリーション API、および Upstash の QStash および Redis を使用すると、自然言語処理を使用してカスタム ストーリーを簡単に生成できます。このチュートリアルに従うことで、これらのツールを使用してストーリーを生成するための独自のシステムをセットアップし、それに独自の変更や改善を加えることができるようになります。
ここでソース コード全体を表示できます。
さらなる改善
このストーリー ジェネレーターを手始めとして、次に何ができるかについて、いくつかのアイデアを以下に示します。
- フロントエンドのスタイルを更新して、より視覚的に魅力的でカラフルになる
- 指定されたプロンプトに基づいて、OpenAI を使用した Dall-E 画像生成をストーリーに追加します
- API 経由で出力を書籍印刷サービスに接続し、ユーザーが物理的な書籍を注文できるようにする
可能性や方向性はたくさんあるので、そのプロセスを楽しんでください。これまでの作業を、OpenAI、QStash、Redis を利用できる他のプロジェクトのベースとして使用することもできます。
-
RedisInsightがRedis開発者にとって完璧なツールである5つの理由
Redisを使用してアプリケーションを構築している開発者にとって、RedisInsightは、単一の使いやすい環境でアプリケーション機能を設計、開発、および最適化するのに役立つ軽量のマルチプラットフォーム管理視覚化ツールです。 RedisInsightは、Redisデータベース用の直感的で効率的なGUIを提供し、データベースとの対話とデータの管理を容易にします。最も一般的なRedisモジュールのサポートが組み込まれています。メモリを分析し、データベース使用量のパフォーマンスをプロファイリングするためのツールを提供し、Redisの使用量を改善するためのガイドを提供します。 GUIを介して、既存の
-
Redis HKEYS –ハッシュ値に含まれるすべてのフィールドの名前を取得する方法
このチュートリアルでは、コマンド– HKEYS を使用して、キーに格納されているハッシュ値に含まれるすべてのフィールドの名前を取得する方法について学習します。 redis-cliで。 キーが存在しない場合は空のリストが返され、キーは存在するがキーに格納されている値がハッシュデータ型ではない場合はエラーが返されます。 redis HKEYSコマンドの構文は次のとおりです:- 構文:- redis host:post> HKEYS <keyname> 出力:- - (array) reply, representing the list of fields in th