Computer >> 컴퓨터 >  >> 프로그래밍 >> Redis

OpenAI, Upstash 및 Next.js를 사용하여 AI 기반 스토리 생성기 구축

이 블로그 게시물의 경우 계속하기 전에 몇 가지 가정을 하겠지만 이상적으로는 다음을 충족해야 합니다.

  • Redis 및 QStash 인스턴스가 생성된 Upstash 계정
  • API 키에 액세스할 수 있는 OpenAI 계정
  • 스토리 생성기 기능을 생성할 Next.js 프로젝트
  • 프로젝트를 배포할 Vercel 계정

소개

AI를 사용하여 나만의 스토리를 만들고 싶었던 적이 있나요? OpenAI의 완성 API와 Upstash의 QStash 및 Redis를 사용하면 이제 자연어 처리를 사용하여 자신만의 맞춤형 스토리를 만드는 것이 그 어느 때보다 쉬워졌습니다. 이 튜토리얼에서는 독특하고 매력적인 스토리를 생성하기 위해 이러한 도구를 설정하고 사용하는 과정을 살펴보겠습니다.

OpenAI, Upstash 및 Next.js를 사용하여 AI 기반 스토리 생성기 구축

앱 이미지 더 보기:

  • 스토리 양식 만들기
  • 스토리 상태 생성
  • 생성된 스토리 표시

건축

코드를 살펴보면 앱이 어떻게 설정되는지 충분히 이해할 수 있지만, 좀 더 높은 수준의 개요로서 아래 이미지는 애플리케이션 흐름의 일부와 통신 방식을 보여줍니다.

OpenAI, Upstash 및 Next.js를 사용하여 AI 기반 스토리 생성기 구축

프로젝트 설정

먼저 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 프로젝트를 배포한 후에는 Upstash 콘솔에서 QStash 및 Redis 토큰을 찾을 수 있고, 여기에서 OpenAI API 키를 찾을 수 있으며, Vercel 대시보드에서 사이트 URL을 찾을 수 있습니다.

프런트엔드 설정

다음으로 스토리 프롬프트를 입력하기 위한 페이지와 양식을 만들어 보겠습니다. 메시지를 위한 텍스트 필드와 제출 버튼이 필요합니다.

스토리 생성

파일: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에서 응답이 오면 엔드포인트에 선택 속성이 포함되어 있으면 폴링 요청이 성공적으로 생성된 스토리를 반환했음을 알고 있으므로 구성 요소는 폴링을 중지하고 스토리 텍스트를 단락으로 분할하고 각 단락을 별도로 렌더링하여 표시합니다.

간격 후크

파일: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 클라이언트를 생성하기 위한 두 개의 파일을 생성합니다. 두 파일 모두 해당 외부 서비스와 상호 작용하는 데 사용되는 개체를 내보냅니다.

파일: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에 저장된 토큰으로 초기화됩니다. 환경 변수. 이 개체는 Upstash QStash 서비스에 HTTP 요청을 보내는 데 사용될 수 있습니다.

파일: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를 활용할 수 있는 다른 프로젝트의 기반으로 지금까지의 작업을 사용할 수도 있습니다.