কম্পিউটার টিউটোরিয়াল

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

AI আরও সহজলভ্য হচ্ছে, Replicate-এর মতো কোম্পানিগুলো মেশিন লার্নিং মডেলগুলো প্রজেক্টে সহজে ইন্টিগ্রেট করা সম্ভব করে দিচ্ছে।

এই আর্টিকেলে, আমি আলোচনা করব CaptionAI তৈরি করেছি কীভাবে, একটি ওয়েব অ্যাপ্লিকেশন যা ব্যবহারকারীদের ছবি আপলোড করতে দেয় এবং AI-জেনারেটেড টেক্সট ক্যাপশন পেতে দেয়। আমি এই প্রজেক্টটি এই Vercel টেমপ্লেট ব্যবহার করে বানিয়েছি। এছাড়াও এটি কীভাবে তৈরি করা হয়েছে সেটি ব্যাখ্যা করে একটি ভিডিওও আছে।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

আমরা কী ব্যবহার করব

  • Next.js 13 (ফ্রন্টএন্ড এবং ব্যাকএন্ড)
  • আপস্ট্যাশ রেডিস (রেট লিমিটিং)
  • Replicate (মেশিন লার্নিং API)
  • Tailwind CSS (স্টাইলিং)
  • Vercel (ডিপ্লয়মেন্ট)

আপনার কী কী লাগবে

  • ডেটাবেস তৈরি করার জন্য একটি আপস্ট্যাশ অ্যাকাউন্ট
  • মেশিন লার্নিং API অ্যাক্সেস করার জন্য একটি Replicate অ্যাকাউন্ট

আপস্ট্যাশ রেডিস সেটআপ

আপস্ট্যাশ অ্যাকাউন্ট তৈরি করে লগইন করলে, আপনি রেডিস ট্যাবে যাবেন এবং একটি ডেটাবেস তৈরি করবেন।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

ডেটাবেস তৈরি হলে, Details ট্যাবে যান। নিচে স্ক্রল করে REST API সেকশন খুঁজুন এবং .env বাটন সিলেক্ট করুন। কন্টেন্ট কপি করে কোথাও সেভ করে রাখুন।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

Replicate সেটআপ

Replicate অ্যাকাউন্ট তৈরি করে লগইন করলে, Account ট্যাবে যান এবং API টোকেন কোথাও সেভ করে রাখুন।

*দ্রষ্টব্য: আপনি Replicate ফ্রি ব্যবহার করতে পারেন, কিন্তু একটু পর আপনাকে ক্রেডিট কার্ড দিতে বলা হবে। দাম মডেলের ওপর নির্ভর করে ভিন্ন হয়। আমরা যে মডেলটি ব্যবহার করছি, salesforce/blip, এটি চালাতে প্রায় $0.00042 খরচ হয়।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

প্রজেক্ট সেটআপ

স্ক্র্যাচ থেকে প্রজেক্ট তৈরি করার পরিবর্তে, আপনি GitHub থেকে রিপোজিটরি ক্লোন করতে পারেন।

রিপো ক্লোন করার পর, আপনি একটি .env ফাইল তৈরি করবেন। .example.env ফাইল থেকে ইনফো কপি করে .env ফাইলে পেস্ট করুন। একবার কপি হয়ে গেলে, উপরের সেকশন থেকে সেভ করা আইটেমগুলো যোগ করুন।

এটি এমন দেখাবে:

// .env
REPLICATE_API_KEY="উপরের থেকে আপনার replicate api key"
// অপশনাল, যদি আপনি রেট লিমিটিং করেন
UPSTASH_REDIS_REST_URL="উপরের থেকে আপনার upstash redis rest url"
UPSTASH_REDIS_REST_TOKEN="উপরের থেকে আপনার upstash redis rest token"

এই ইনফো যোগ করার পর, আপনি টার্মিনালে নিচের কমান্ডগুলো দিয়ে প্রজেক্ট রান করতে পারবেন:

npm install
npm run dev

রিপোজিটরি স্ট্রাকচার

এটি প্রজেক্টের মূল ফোল্ডার স্ট্রাকচার। লাল ঘेড়ে দেওয়া ফাইলগুলো এই পোস্টে আরও আলোচনা করা হবে যা ছবি আপলোড, রেট লিমিটিং এবং BLIP ML API বাস্তবায়ন নিয়ে কাজ করে।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

উচ্চ-স্তরের ডেটা ফ্লো

এটি একটি হাই-লেভেল ডায়াগ্রাম কীভাবে ডেটা ফ্লো করছে। আমাদের ইনপুট, ব্যবহারকারী-আপলোড করা ছবি, আপলোড কম্পোনেন্ট থেকে, ব্যাকএন্ডে BLIP ML API প্রসেসিং-এর মাধ্যমে যায় এবং তারপর UI-তে রেসপন্স টেক্সট প্রদর্শিত হয়।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

রেডিস ইন্সট্যান্স তৈরি

প্রজেক্টের ভিতরে, আমরা আমাদের আপস্ট্যাশ রেডিস ক্লায়েন্ট সেটআপ করব যা প্রজেক্টে যেখানে প্রয়োজন সেখান থেকে রেফারেন্স করা যাবে।

// `/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 মডিউল ইম্পোর্ট করে এবং একটি নতুন রেডিস ইন্সট্যান্স তৈরি করে। ইন্সট্যান্সটি দুটি এনভায়রনমেন্ট ভেরিয়েবল, UPSTASH_REDIS_REST_URL এবং UPSTASH_REDIS_REST_TOKEN-এর উপস্থিতির ওপর শর্তসাপেক্ষভাবে তৈরি হয়।

যদি দুটি ভেরিয়েবলই সংজ্ঞায়িত থাকে, নির্দিষ্ট URL এবং টোকেন দিয়ে একটি নতুন রেডিস ইন্সট্যান্স তৈরি হয়। যদি যেকোনো একটি বা দুটি ভেরিয়েবল undefined থাকে, redis ভেরিয়েবলটি undefined সেট হয়। অবশেষে, 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",
      error: "#d23f4d",
      shade100: "#fff",
      shade200: "#fffe",
      shade300: "#fffd",
      shade400: "#fffc",
      shade500: "#fff9",
      shade600: "#fff7",
      shade700: "#fff2",
      shade800: "#fff1",
      shade900: "#ffff",
    },
  },
  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" মান ব্যবহার করা হয়।

options অবজেক্টে আপলোডারের জন্য বিভিন্ন অপশন থাকে। এগুলোর মধ্যে:

  • maxFileCount: একবারে আপলোড করা ফাইলগুলোর সর্বোচ্চ সংখ্যা ১ সেট করে।
  • 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!");
    // টেক্সট কপি হলে বাটন টেক্সট "Copied!" সেট করা
    setTimeout(() => {
      setButtonText("Copy"); // ২ সেকেন্ড পর বাটন টেক্সট আবার "Copy" করা
    }, 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 মেথড ব্যবহার করে caption ভেরিয়েবলটি ক্লিপবোর্ডে কপি করে। টেক্সট কপি হলে, এটি buttonText ভেরিয়েবলকে দুই সেকেন্ডের জন্য "Copied!"-এ পরিবর্তন করে, তারপর আবার "Copy"-এ রিসেট করে।

একটি সাব-কম্পোনেন্টও আছে UploadDropZone যা নির্দিষ্ট uploader এবং options-এ UploadDropzone কম্পোনেন্টের একটি ইন্সট্যান্স রেন্ডার করে। onUpdate কলব্যাক originalPhoto এবং caption ভেরিয়েবলগুলো আপলোড করা ছবির URL এবং জেনারেট করা ক্যাপশন দিয়ে আপডেট করতে ব্যবহৃত হয়।

অবশেষে, একটি অ্যাসিনক্রোনাস ফাংশন আছে generateCaption যা fileUrl প্যারামিটার নেয়, যা আপলোড করা ছবির URL। এটি fetch ব্যবহার করে /api/generate এন্ডপয়েন্টে POST রিকোয়েস্ট করে এবং JSON পেলোড হিসেবে fileUrl পাস করে। রেসপন্সটি JSON হিসেবে পার্স করা হয় এবং রেসপন্স সফল হয় কিনা তার ওপর ভিত্তি করে caption বা error ভেরিয়েবল সেট করা হয়। loading ভেরিয়েবলও আপডেট হয় রিকোয়েস্ট চলছে কিনা তা ইন্ডিকেট করার জন্য। ফাংশনটিতে setTimeout ব্যবহার করে ৫০০ms এর একটি ডিলেও আছে API রেট লিমিট এড়ানোর জন্য।

রেট লিমিটিং

// `/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; };
}
// একটি নতুন রেটলিমিটার তৈরি, যা প্রতি ১৫ মিনিটে ৩ রিকোয়েস্ট অনুমতি দেয়
const ratelimit = redis
  ? new Ratelimit({
      redis: redis,
      limiter: Ratelimit.fixedWindow(5, "1440 m"),
      analytics: true,
    })
  : undefined;
...

এই কোড Next.js-এ একটি API এন্ডপয়েন্ট তৈরি করার জন্য প্রয়োজনীয় মডিউল এবং টাইপ ইম্পোর্ট করে, সেইসাথে আপস্ট্যাশ রেডিস ডেটাবেস ক্লায়েন্ট এবং "@upstash/ratelimit" নামে একটি রেট লিমিটার লাইব্রেরি।

ratelimit কনস্ট্যান্ট Ratelimit ক্লাসের একটি নতুন ইন্সট্যান্স তৈরি করে, যা একটি ফিক্সড-উইন্ডো রেট লিমিটার তৈরি করে যা প্রতি ১৪৪০ মিনিটে (২৪ ঘণ্টা) ৫ রিকোয়েস্ট অনুমতি দেয়। redis প্রপার্টি Ratelimit কনস্ট্রাক্টরে প্যারামিটার হিসেবে পাস করা হয় অ্যাপ্লিকেশনের একাধিক ইন্সট্যান্স জুড়ে রেট লিমিটিং সক্ষম করতে। যদি redis undefined থাকে (যেমন, যদি রেডিস ডেটাবেস কনফিগার করা না থাকে), ratelimit ও undefined সেট হয়। এর মানে রেডিস উপলব্ধ না থাকলে রেট লিমিটিং প্রয়োগ করা হবena।

// `/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 হ্যান্ডলার ফাংশন অংশ যা একটি ক্লায়েন্ট যে রিকোয়েস্ট করতে পারে সেটার রেট লিমিট করে। এটি প্রথমে চেক করে রেট লিমিটার ইন্সট্যান্স উপলব্ধ কিনা এবং যদি হয়, request-ip প্যাকেজ ব্যবহার করে ক্লায়েন্টের IP ঠিকানা এক্সট্র্যাক্ট করে এবং এটি ratelimit.limit মেথডে পাস করে। এই মেথড একটি অবজেক্ট ফেরত দেয় যাতে নির্দিষ্ট টাইমফ্রেমের মধ্যে বাকি রিকোয়েস্টের সংখ্যা এবং রিকোয়েস্টটি সফল হয়েছে কিনা থাকে।

যদি রিকোয়েস্ট সফল হয়, X-RateLimit-Limit এবং X-RateLimit-Remaining হেডারগুলো রেসপন্সে সেট করা হয়। যদি রিকোয়েস্ট লিমিট এক্সিড হয়ে যায়, ৪২৯ স্ট্যাটাস কোড এবং একটি এরর মেসেজ রেসপন্সে পাঠানো হয়, এবং ফাংশনটি আড়িয়ালি রিটার্ন করে আরও এক্সিকিউশন রোধ করার জন্য।

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 নেয় এবং "https://api.replicate.com/v1/predictions" এন্ডপয়েন্টে একটি POST রিকোয়েস্ট পাঠায় ইমেজ ক্যাপশন পেতে image_captioning টাস্ক ব্যবহার করে। রিকোয়েস্টে Authorization হেডার থাকে যেটে Replicate API কী থাকে অথেন্টিকেশনের জন্য, এবং Content-Type হেডার "application/json"-এ সেট করা থাকে। API থেকে রেসপন্স JSON হিসেবে পার্স করা হয় এবং jsonStartResponse অবজেক্ট থেকে endpointUrl এক্সট্র্যাক্ট করা হয়।

আপনি মডেলের ভার্সন নম্বর খুঁজে পেতে পারেন আপনি যে মডেলটি ব্যবহার করতে চান সেটি সিলেক্ট করে।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

API ট্যাব সিলেক্ট করুন।

Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

নিচে স্ক্রল করুন যতক্ষণ না ভার্সন নম্বর দেখাচ্ছে, লাল রঙে আউটলাইন করা। এবং নীল রঙে আউটলাইন করা ইনপুট প্যারামিটারগুলো যা আপনি ব্যবহার করতে পারেন। Next.js, Replicate এবং Redis দিয়ে AI-চালিত ইমেজ ক্যাপশনিং অ্যাপ তৈরি করুন

// `/pages/api/generate.ts` continued
let jsonStartResponse = await startResponse.json();
let endpointUrl = jsonStartResponse.urls.get;
// GET রিকোয়েস্ট ইমেজ রিস্টোরেশন প্রসেসের স্ট্যাটাস পেতে এবং রেজাল্ট রেডি হলে ফেরত দিতে
let caption: string | null = null;
while (!caption) {
  // ১ সেকেন্ড ইন্টারভালে লুপ করুন যতক্ষণ না অল্ট টেক্সট রেডি হয়
  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 লুপ ব্যবহার করা হয় endpointUrl-কে ১-সেকেন্ড ইন্টারভালে পোল করার জন্য যতক্ষণ না ক্যাপশন রেডি হয়। লুপটি একই Authorization এবং Content-Type হেডার সহ endpointUrl-এ একটি GET রিকোয়েস্ট পাঠায়, এবং রেসপন্সও JSON হিসেবে পার্স করা হয়। যদি jsonFinalResponse অবজেক্টে status "succeeded" হয়, ক্যাপশন output প্রপার্টি থেকে এক্সট্র্যাক্ট করা হয়। যদি status "failed" হয়, লুপটি টার্মিনেট হয়। যদি status "succeeded" বা "failed" না হয়, লুপটি setTimeout মেথড ব্যবহার করে ১ সেকেন্ড অপেক্ষা করে আবার পোল করে।

শেষে, ক্যাপশনটি JSON রেসপন্স হিসেবে স্ট্যাটাস কোড ২০০ সহ ফেরত দেওয়া হয় যদি এটি null না হয়, অন্যথায় "Failed to generate caption" মেসেজসহ একটি রেসপন্স স্ট্যাটাস কোড ২০০ সহ ফেরত দেওয়া হয়।

উপসংহার

উপসংহারে, এই প্রজেক্টটি ইমেজ আপলোডিং, রেট লিমিটিং এবং মেশিন লার্নিং API ইন্টিগ্রেট করার বিষয়ে মূল্যবান অভিজ্ঞতা দিয়েছে। এই প্রজেক্টটি সফলভাবে সম্পন্ন করে, আমরা এই টেকনোলজিগুলো এবং ভবিষ্যতে আরও উন্নত প্রজেক্ট তৈরি করতে এগুলোর ব্যবহার সম্পর্কে আরও ভালো বোঝাপড়া পেয়েছি।

  1. বাইনারি ট্রি মধ্যে বাইনারি সার্চ ট্রির সংখ্যা গণনা C++-এ

  2. জাভাস্ক্রিপ্টে ফর্ম ভ্যালিডেশন: খালি ইনপুট ফিল্ড চেক করা

  3. Guider – লিনাক্স সিস্টেম পারফরম্যান্স বিশ্লেষণের চূড়ান্ত টুল

  4. HTML DOM Article অবজেক্ট