Với việc AI ngày càng dễ tiếp cận hơn, các công ty như Replicate đã giúp việc tích hợp các mô hình máy học vào các dự án một cách liền mạch dễ dàng hơn.
Trong bài viết này, tôi sẽ thảo luận về cách tôi xây dựng CaptionAI, một ứng dụng web cho phép người dùng tải hình ảnh lên và nhận chú thích văn bản do AI tạo. Tôi đã xây dựng dự án này bằng mẫu Vercel này. Ngoài ra còn có video này giải thích cách xây dựng dự án này.

Những gì chúng tôi sẽ sử dụng
- Next.js 13 (Front-end và Back-end)
- Upstash Redis (Giới hạn tỷ lệ)
- Sao chép (API học máy)
- CSS Tailwind (Tạo kiểu)
- Vercel (Triển khai)
Những gì bạn cần
- Tài khoản Upstash để tạo cơ sở dữ liệu
- Tài khoản sao chép để truy cập API Machine Learning
Thiết lập Upstash Redis
Khi bạn đã tạo tài khoản Upstash và đăng nhập, bạn sẽ chuyển tới tab Redis và tạo cơ sở dữ liệu.


Sau khi tạo cơ sở dữ liệu, bạn sẽ chuyển đến tab Chi tiết. Cuộn xuống cho đến khi bạn tìm thấy phần REST API và chọn nút .env. Sao chép nội dung và lưu ở nơi an toàn.

Thiết lập Sao chép
Khi bạn đã tạo tài khoản Sao chép và đăng nhập, bạn sẽ chuyển tới tab Tài khoản và lưu mã thông báo API ở nơi an toàn.
*Lưu ý:Bạn có thể sử dụng Replica miễn phí nhưng sau một thời gian, bạn sẽ được yêu cầu nhập thẻ tín dụng của mình. Giá thay đổi tùy thuộc vào mô hình bạn sử dụng. Mô hình chúng tôi đang sử dụng, salesforce/blip, tốn khoảng 0,00042 USD để chạy.

Thiết lập dự án
Thay vì tạo dự án từ đầu, bạn có thể sao chép kho lưu trữ từ GitHub.
Khi bạn đã sao chép kho lưu trữ, bạn sẽ tạo một tệp .env. Sao chép thông tin từ tệp .example.env vào tệp .env. Sau khi sao chép xong, bạn sẽ thêm các mục mà chúng tôi đã lưu từ các phần trên.
Nó sẽ trông giống như thế này:
// .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" Khi bạn đã bao gồm thông tin này, bạn sẽ có thể chạy dự án bằng cách nhập các lệnh này vào terminal:
npm install npm run dev Cấu trúc kho lưu trữ
Đây là cấu trúc thư mục chính của dự án. Tôi đã khoanh tròn màu đỏ các tệp sẽ được thảo luận thêm trong bài đăng này liên quan đến việc tải hình ảnh lên, giới hạn tỷ lệ và triển khai BLIP ML API.

Luồng dữ liệu cấp cao
Đây là sơ đồ cấp cao về cách dữ liệu được truyền đi. Đầu vào của chúng tôi, hình ảnh do người dùng tải lên, đi qua Thành phần tải lên, tới phần phụ trợ để xử lý API BLIP ML và sau đó hiển thị văn bản phản hồi trong giao diện người dùng.

Tạo phiên bản Redis
Bên trong dự án, chúng ta sẽ thiết lập ứng dụng khách redis mới nhất mà chúng ta có thể tham khảo trong suốt dự án khi cần thiết.
// `/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; Đoạn mã này nhập mô-đun Redis từ gói "@upstash/redis" và tạo một phiên bản Redis mới. Phiên bản được tạo có điều kiện dựa trên sự hiện diện của hai biến môi trường, UPSTASH_REDIS_REST_URL và UPSTASH_REDIS_REST_TOKEN.
Nếu cả hai biến đều được xác định, một phiên bản Redis mới sẽ được tạo bằng URL và mã thông báo được chỉ định. Nếu một hoặc cả hai biến không được xác định thì biến redis được đặt thành không xác định. Cuối cùng, biến redis được xuất từ mô-đun để sử dụng trong các phần khác của ứng dụng.
Tải hình ảnh lên
// `/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";
},
}; Mã này đặt các tùy chọn cấu hình cho thành phần trình tải lên. Trình tải lên được tạo bằng hàm Uploader() và các tùy chọn được chuyển dưới dạng đối tượng cho nó.
Tùy chọn cấu hình đầu tiên là apiKey được sử dụng để xác thực với dịch vụ tải lên. Giá trị của apiKey được xác định dựa trên việc biến môi trường NEXT_PUBLIC_UPLOAD_API_KEY có được đặt hay không. Nếu được đặt, giá trị của biến môi trường sẽ được sử dụng, nếu không, giá trị "miễn phí" sẽ được sử dụng.
Đối tượng tùy chọn chứa nhiều tùy chọn khác nhau cho người tải lên. Chúng bao gồm:
- maxFileCount:Đặt số lượng tệp tối đa có thể được tải lên cùng một lúc là 1.
- mimeTypes:Đặt loại MIME được phép cho các tệp được tải lên thành "image/jpeg", "image/png" và "image/jpg".
- trình chỉnh sửa:Định cấu hình các tùy chọn cho trình chỉnh sửa hình ảnh, tùy chọn này bị tắt trong trường hợp này bằng cách đặt cắt xén thành sai.
- styles:Xác định kiểu tùy chỉnh cho giao diện người dùng của người tải lên.
- onValidate:Xác định một hàm được gọi để xác thực từng tệp trước khi nó được tải lên. Trong trường hợp này, hàm này sử dụng NSFWPredictor để kiểm tra xem hình ảnh có an toàn cho công việc hay không. Nếu hình ảnh không an toàn, một thông báo lỗi sẽ được trả về cho biết hình ảnh đó không được phép.
// `/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);
}
...
Có một số biến trạng thái được xác định bằng hook useState.
originalPhotolà một chuỗi đại diện cho URL của hình ảnh được tải lên.captionlà một chuỗi chứa chú thích được tạo cho hình ảnh được tải lên.buttonTextlà một chuỗi đại diện cho văn bản trên nút sao chép.loadinglà một boolean cho biết thành phần hiện có đang tìm nạp dữ liệu hay không.errorlà một chuỗi chứa thông báo lỗi nếu có lỗi xảy ra trong quá trình tạo phụ đề.
Thành phần này có một hàm gọi là copyToClipboard, hàm này sử dụng phương thức navigator.clipboard.writeText để sao chép biến chú thích vào bảng nhớ tạm. Khi văn bản được sao chép, nó sẽ thay đổi biến nút văn bản thành "Đã sao chép!" trong hai giây trước khi đặt lại thành "Sao chép".
Ngoài ra còn có một thành phần phụ được gọi là UploadDropZone hiển thị một phiên bản của thành phần UploadDropzone với các tùy chọn và trình tải lên được chỉ định. Lệnh gọi lại onUpdate được sử dụng để cập nhật các biến ảnh gốc và chú thích bằng URL của hình ảnh đã tải lên và chú thích được tạo.
Cuối cùng, có một hàm không đồng bộ có tên là generateCaption nhận tham số fileUrl, đây là URL của hình ảnh được tải lên. Nó sử dụng tìm nạp để gọi điểm cuối /api/generate bằng yêu cầu POST và chuyển vào fileUrl dưới dạng tải trọng JSON. Sau đó, phản hồi được phân tích cú pháp dưới dạng JSON và đặt biến chú thích hoặc lỗi, tùy thuộc vào việc phản hồi có thành công hay không. Biến tải cũng được cập nhật để cho biết liệu yêu cầu có còn được thực hiện hay không. Hàm này cũng bao gồm độ trễ 500 mili giây bằng cách sử dụng hàm setTimeout để tránh đạt đến giới hạn tốc độ API.
Giới hạn tỷ lệ
// `/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;
... Mã này nhập các mô-đun và loại cần thiết để tạo điểm cuối API trong Next.js, cũng như ứng dụng khách cơ sở dữ liệu Upstash Redis và thư viện bộ giới hạn arate có tên là "@upstash/ratelimit".
Hằng số giới hạn tốc độ tạo một phiên bản mới của lớp Ratelimit, tạo ra một bộ giới hạn tốc độ cửa sổ cố định cho phép 5 yêu cầu cứ sau 1440 phút (24 giờ). Thuộc tính redis được chuyển dưới dạng tham số cho hàm tạo Ratelimit để kích hoạt giới hạn tốc độ trên nhiều phiên bản của ứng dụng. Nếu redis không được xác định (ví dụ:nếu cơ sở dữ liệu Redis không được định cấu hình), thì giới hạn tốc độ cũng được đặt thành không xác định. Điều này có nghĩa là giới hạn tốc độ sẽ không được áp dụng nếu Redis không có sẵn.
// `/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;
}
}
... Khối mã này là một phần của chức năng xử lý API nhằm giới hạn tỷ lệ yêu cầu mà khách hàng có thể thực hiện đối với API. Trước tiên, nó sẽ kiểm tra xem có sẵn phiên bản giới hạn tốc độ hay không và nếu có, hãy trích xuất địa chỉ IP của máy khách bằng cách sử dụng gói request-ip và chuyển nó sang phương thức ratelimit.limit. Phương thức này trả về một đối tượng chứa số lượng yêu cầu còn lại trong khung thời gian đã chỉ định và yêu cầu đó có thành công hay không.
Nếu yêu cầu thành công, các tiêu đề X-RateLimit-Limitand X-RateLimit-Remaining sẽ được đặt trong phản hồi. Nếu vượt quá giới hạn yêu cầu, mã trạng thái 429 và thông báo lỗi sẽ được gửi trong phản hồi và hàm sẽ trả về sớm để ngăn việc thực thi thêm.
API BLIP ML
// `/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",
},
}),
});
...
Phần mã này lấy imageUrl từ nội dung yêu cầu và gửi yêu cầu POST đến điểm cuối "https://api.replicate.com/v1/predictions" để nhận chú thích hình ảnh bằng cách sử dụng tác vụ image_captioning. Yêu cầu này bao gồm tiêu đề Cấp phép chứa khóa API Sao chép để xác thực và tiêu đề Loại nội dung được đặt thành "application/json". Phản hồi từ API được phân tích cú pháp dưới dạng JSON và endpointUrl được trích xuất từ đối tượng jsonStartResponse.
Bạn có thể tìm thấy số phiên bản của mô hình bằng cách chọn mô hình bạn muốn sử dụng.

Chọn tab API.

Cuộn xuống cho đến khi số phiên bản hiển thị, viền màu đỏ. Và viền màu xanh lam là các thông số đầu vào bạn có thể sử dụng. 
// `/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");
} Tiếp theo, vòng lặp while được sử dụng để thăm dò endpointUrl trong khoảng thời gian 1 giây cho đến khi chú thích sẵn sàng. Vòng lặp gửi yêu cầu GET đến endpointUrl có cùng tiêu đề Ủy quyền và Loại nội dung, đồng thời phản hồi cũng được phân tích cú pháp dưới dạng JSON. Nếu trạng thái trong đối tượng jsonFinalResponse là "thành công", chú thích sẽ được trích xuất từ thuộc tính đầu ra. Nếu trạng thái là "không thành công", vòng lặp sẽ kết thúc. Nếu trạng thái không phải là "thành công" hay "không thành công", vòng lặp sẽ đợi 1 giây bằng phương thức setTimeout trước khi thăm dò lại.
Cuối cùng, chú thích được trả về dưới dạng phản hồi JSON có mã trạng thái là 200 nếu không phải là rỗng, nếu không, phản hồi có thông báo "Không tạo được chú thích" sẽ được trả về với mã trạng thái là 200.
Kết luận
Tóm lại, dự án này đã cung cấp kinh nghiệm quý báu trong việc triển khai tải hình ảnh lên, giới hạn tỷ lệ và tích hợp API máy học. Khi hoàn thành thành công dự án này, chúng tôi đã hiểu rõ hơn về các công nghệ này và cách chúng có thể được sử dụng để tạo các dự án nâng cao hơn trong tương lai.