Đối với bài đăng blog này, chúng tôi sẽ đưa ra một số giả định trước khi tiếp tục, nhưng lý tưởng nhất là bạn nên có:
- Tài khoản Upstash nơi bạn đã tạo phiên bản Redis và QStash
- Tài khoản OpenAI có quyền truy cập vào khóa API của bạn
- Dự án Next.js nơi chúng tôi sẽ tạo chức năng tạo câu chuyện
- Tài khoản Vercel để triển khai dự án của bạn
Giới thiệu
Bạn đã bao giờ muốn tạo câu chuyện của riêng mình bằng AI chưa? Với API hoàn thiện của OpenAI cũng như QStash và Redis của Upstash, giờ đây việc tạo các câu chuyện tùy chỉnh của riêng bạn bằng cách xử lý ngôn ngữ tự nhiên trở nên dễ dàng hơn bao giờ hết. Trong hướng dẫn này, chúng ta sẽ hướng dẫn quy trình thiết lập và sử dụng những công cụ này để tạo ra những câu chuyện độc đáo và hấp dẫn.

Xem thêm hình ảnh của ứng dụng:
- Tạo mẫu câu chuyện
- Tạo trạng thái câu chuyện
- Câu chuyện đã tạo được hiển thị
Kiến trúc
Bạn có thể hiểu rõ về cách thiết lập ứng dụng khi xem qua mã, nhưng để có cái nhìn tổng quan ở cấp độ cao hơn, có hình ảnh bên dưới hiển thị một số phần của quy trình ứng dụng và cách chúng giao tiếp.

Thiết lập dự án
Đầu tiên chúng ta sẽ tạo một dự án Next.js. Điều này có thể được thực hiện bằng cách chạy lệnh sau để tạo dự án Next.js mới với TypeScript. Bạn có thể tìm hiểu các bước thiết lập Next.js tại đây.
Vì mục đích của hướng dẫn này, chúng tôi cũng đã cài đặt CSS Tailwind (cả biểu mẫu và kiểu chữ), nhưng điều đó hoàn toàn không bắt buộc và chỉ dành cho kiểu dáng biểu mẫu giao diện người dùng.
Tiếp theo, chúng tôi sẽ cài đặt thư viện QStash và Redis của Upstash thông qua:
npm install @upstash/qstash
npm install @upstash/redis
Bây giờ bạn sẽ muốn tạo một .env.local tệp và điền vào đó các khóa sau (và các giá trị từ những vị trí có liên quan).
SITE_URL=https://your-project-url.vercel.app
OPENAI_API_KEY=
QSTASH_TOKEN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN= Bạn có thể tìm thấy mã thông báo QStash và Redis trong bảng điều khiển Upstash, khóa API OpenAI tại đây và URL trang web của bạn trong bảng điều khiển Vercel sau khi bạn đã tạo dự án và triển khai dự án Next.js cơ bản.
Thiết lập giao diện người dùng
Tiếp theo, chúng ta sẽ tạo trang và biểu mẫu để nhập lời nhắc câu chuyện. Bạn sẽ cần một trường văn bản cho lời nhắc và nút gửi.
Sáng tạo câu chuyện
Tập tin: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>
</>
);
}
Tệp này xác định thành phần React hiển thị biểu mẫu cho phép người dùng nhập chủ đề, nhân vật và đạo đức cho một câu chuyện. Khi biểu mẫu được gửi, nó sẽ gửi POST yêu cầu tới /api/create điểm cuối với chủ đề, tính cách và giá trị đạo đức được nhập vào làm nội dung.
Sau đó, thành phần này sẽ chuyển sang trạng thái thăm dò ý kiến và gửi GET yêu cầu tới /api/poll điểm cuối mỗi giây cùng với mã nhận dạng tin nhắn nhận được trong yêu cầu tạo câu chuyện trước đó, cho phép chúng tôi theo dõi yêu cầu tạo câu chuyện đối với câu chuyện mà chúng tôi đang thăm dò để kiểm tra xem khi nào nó đã được OpenAI tạo xong.
Khi có phản hồi từ /api/poll điểm cuối chứa thuộc tính lựa chọn, chúng tôi biết rằng yêu cầu bỏ phiếu đã trả về một câu chuyện được tạo thành công, do đó thành phần này dừng bỏ phiếu và hiển thị văn bản câu chuyện bằng cách chia nó thành các đoạn và hiển thị từng đoạn riêng biệt.
Móc khoảng thời gian
Tập tin: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 hook sử dụng useEffect và useRef hooks để quản lý khoảng thời gian và chức năng gọi lại hoạt động liền mạch với vòng đời của thành phần React, cũng như cung cấp một cách thuận tiện để quản lý khoảng thời gian và cuộc gọi lại trong thành phần React, tối ưu hóa hiệu suất và làm cho cơ sở mã dễ bảo trì hơn một chút. Bạn có thể tìm thêm thông tin về hook này tại đây và tại đây.
Thiết lập API
Trước tiên, chúng ta sẽ tạo lệnh gọi lại, thăm dò ý kiến và tạo tệp cũng như cách sử dụng thư viện Redis và QStash.
Sáng tạo câu chuyện
Tập tin: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 });
});
}
Trước tiên chúng tôi kiểm tra xem phương thức yêu cầu có phải là POST không và gửi phản hồi có mã trạng thái là 400 (cho biết lỗi máy khách) nếu không. Sau đó, chúng tôi tiến hành phân tách chủ đề, tính cách và các lĩnh vực đạo đức khỏi nội dung yêu cầu.
Tiếp theo chúng ta gọi publishJSON phương pháp trên qstashClient đối tượng sẽ gửi POST yêu cầu API OpenAI có nội dung JSON chứa lời nhắc tạo câu chuyện dành cho trẻ em dựa trên các giá trị về chủ đề, nhân vật và đạo đức. Nó cũng đặt một số tiêu đề, bao gồm tiêu đề ủy quyền với mã thông báo được lưu trữ trong QSTASH_TOKEN biến môi trường và tiêu đề ủy quyền được chuyển tiếp để chuyển qua OPENAI_API_KEY sẽ được sử dụng cùng với yêu cầu API OpenAI.
Sau đó, chúng tôi trả về ID tin nhắn của yêu cầu nếu publishJSON cuộc gọi thành công, cuộc gọi này sẽ được sử dụng để bỏ phiếu để kiểm tra khi nào yêu cầu kết thúc. Nếu xảy ra lỗi, nó sẽ gửi phản hồi với mã trạng thái 500 (cho biết lỗi máy chủ nội bộ) và thông báo lỗi liên quan.
Gọi lại
Tập tin: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 });
}
} Trước hết, trước tiên, chúng tôi cố gắng giải mã phần nội dung của yêu cầu đến, đây sẽ là chuỗi được mã hóa base64 và nếu thành công, nó sẽ lưu chuỗi đã giải mã trong Redis dưới cùng khóa với chuỗi được trả về khi chúng tôi gửi yêu cầu ban đầu tới QStash.
Cuối cùng, chúng tôi gửi phản hồi có mã trạng thái là 200 (cho biết thành công) cũng như chuỗi được giải mã. Nếu có bất kỳ lỗi nào xảy ra, chúng tôi sẽ trả về phản hồi có mã trạng thái là 500 (cho biết lỗi máy chủ nội bộ) và thông báo lỗi.
Bỏ phiếu
Tập tin: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 });
}
}
Đầu tiên, chúng ta hủy cấu trúc id từ đối tượng truy vấn của yêu cầu. Sau đó, chúng tôi cố gắng truy xuất dữ liệu được lưu trữ trong Redis theo id bị phá hủy và nếu không tìm thấy dữ liệu, nó sẽ gửi phản hồi với mã trạng thái 404 (cho biết rằng không thể tìm thấy tài nguyên được yêu cầu) và một thông báo cho biết như vậy.
Nếu dữ liệu được tìm thấy thuộc về khóa đã cho, nó sẽ gửi phản hồi với mã trạng thái là 200 (cho biết thành công) và dữ liệu tìm thấy cùng với nó. Nếu có bất kỳ lỗi nào xảy ra, chúng tôi sẽ trả về phản hồi có mã trạng thái 500 (cho biết lỗi máy chủ nội bộ) và thông báo lỗi liên quan.
Lib
Tiếp theo, chúng tôi sẽ tạo hai tệp để tạo ứng dụng khách QStash và Redis, được sử dụng trong quy trình tạo câu chuyện. Cả hai tệp đều xuất một đối tượng được sử dụng để tương tác với dịch vụ bên ngoài tương ứng.
Tập tin:lib/qstash.ts
import { Client } from "@upstash/qstash";
const qstashClient = new Client({
token: process.env.QSTASH_TOKEN as string,
});
export default qstashClient;
Ứng dụng khách QStash được khởi tạo bằng mã thông báo được lưu trữ trong QSTASH_TOKEN biến môi trường. Đối tượng này có thể được sử dụng để gửi yêu cầu HTTP đến dịch vụ Upstash QStash.
Tập tin: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;
Ứng dụng khách Redis được khởi tạo với URL và mã thông báo được lưu trữ trong UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN các biến môi trường tương ứng. Đối tượng này có thể được sử dụng để lưu trữ và truy xuất dữ liệu trong cơ sở dữ liệu Redis thông qua API Upstash Redis REST.
Kết luận
Với API hoàn thành của OpenAI, cũng như QStash và Redis của Upstash, thật dễ dàng tạo các câu chuyện tùy chỉnh bằng cách xử lý ngôn ngữ tự nhiên. Bằng cách làm theo hướng dẫn này, giờ đây bạn có thể thiết lập hệ thống của riêng mình để tạo câu chuyện bằng các công cụ này và thực hiện các thay đổi cũng như cải tiến của riêng bạn trên hệ thống đó.
Bạn có thể xem toàn bộ mã nguồn tại đây.
Cải tiến hơn nữa
Dưới đây là một số ý tưởng về những việc bạn có thể làm tiếp theo khi bắt đầu sử dụng trình tạo câu chuyện này:
- Cập nhật kiểu dáng giao diện người dùng để trở nên hấp dẫn và nhiều màu sắc hơn về mặt hình ảnh
- Thêm tính năng tạo hình ảnh Dall-E bằng OpenAI vào các câu chuyện dựa trên lời nhắc nhất định
- Kết nối đầu ra với dịch vụ in sách thông qua API để người dùng có thể đặt mua sách thực
Có rất nhiều khả năng và hướng đi mà bạn có thể thực hiện, vì vậy hãy vui vẻ và tận hưởng quá trình này. Bạn thậm chí có thể sử dụng tác phẩm này làm cơ sở cho các dự án khác có thể sử dụng OpenAI, QStash và Redis.