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

প্রতিলিপি, Next.js, এবং Upstash ব্যবহার করে একটি ফটো পুনরুদ্ধার অ্যাপ তৈরি করুন

এই ব্লগ পোস্টের জন্য, আমরা চালিয়ে যাওয়ার আগে কয়েকটি অনুমান করব, তবে আপনার আদর্শভাবে থাকা উচিত:

  • একটি Upstash অ্যাকাউন্ট যেখানে আপনি একটি Redis ইনস্ট্যান্স তৈরি করেছেন
  • আপনার API টোকেন অ্যাক্সেস সহ একটি প্রতিলিপি অ্যাকাউন্ট
  • আমাদের পছন্দসই কার্যকারিতা বাস্তবায়নের জন্য একটি Next.js প্রকল্প
  • আপনার প্রোজেক্ট স্থাপন করার জন্য একটি Vercel অ্যাকাউন্ট

এটা কি?

আপনি কি প্রতিলিপিতে উপলব্ধ মডেলগুলি থেকে ছবি তৈরি করতে মেশিন লার্নিং ব্যবহার শুরু করতে চান? ঠিক আছে, এই টিউটোরিয়ালে, আমরা রেপ্লিকেটের হোস্ট করা মডেলের বিস্তৃত পরিসর এবং Upstash এর Redis অন্বেষণ করব। আমরা শুধুমাত্র এই মডেলগুলি অন্বেষণ করব না, তবে আমরা একটি সেট আপ করার প্রক্রিয়ার মধ্য দিয়ে হেঁটে যাব এবং অন্যান্য মডেলগুলি ব্যবহার করার জন্য আপনি কীভাবে সহজেই বাস্তবায়ন আপডেট করতে পারেন তা স্পর্শ করব৷

এই টিউটোরিয়ালে, আমরা Microsoft-এর Bringing Old Photos Back to Life মডেলের ব্যবহার কভার করব, যা মূলত একটি পুরানো ছবি তোলে, মডেলের মাধ্যমে চালায় এবং আপনার ছবির একটি সম্পাদিত, এবং আশা করি উন্নত সংস্করণ আউটপুট করে৷

প্রতিলিপি, Next.js, এবং Upstash ব্যবহার করে একটি ফটো পুনরুদ্ধার অ্যাপ তৈরি করুন

স্থাপত্য কি?

যদি আপনার কিছু প্রতিক্রিয়ার অভিজ্ঞতা থাকে, তাহলে আপনি কোডবেসের মাধ্যমে সহজভাবে পড়ে অ্যাপ আর্কিটেকচার কীভাবে কাজ করে তা নির্ধারণ করতে সক্ষম হবেন, কিন্তু এটিকে কিছুটা সহজ করতে, অথবা আপনি যদি একটি ওভারভিউ দেখতে পছন্দ করেন তবে নীচে একটি দেওয়া আছে৷

প্রতিলিপি, Next.js, এবং Upstash ব্যবহার করে একটি ফটো পুনরুদ্ধার অ্যাপ তৈরি করুন

শুরু করতে আমার কি দরকার?

শুরু করার জন্য, আপনার অবশ্যই একটি Next.js প্রকল্পের প্রয়োজন হবে। এটি এখানে Next.js সেটআপ গাইড অনুসরণ করে করা যেতে পারে, অথবা আপনার যদি ইতিমধ্যেই একটি সেটআপ থাকে, তাহলে সেটাও ঠিক আছে। এই টিউটোরিয়ালে, আমরা টেইলউইন্ড সিএসএসও ব্যবহার করছি, তবে আপনি অবশ্যই আপনার পছন্দের যেকোনো ধরনের স্টাইলিং সেটআপ ব্যবহার করতে পারেন।

এখন যেহেতু আমাদের কাছে একটি বেসিক Next.js প্রজেক্ট সেটআপ আছে, আমরা এখনও কমান্ডটি চালিয়ে Upstash এর Redis লাইব্রেরি করতে পারি:

npm install @upstash/redis

পরবর্তীতে, আমরা আমাদের .env.local পূরণ করতে চাই নিম্নলিখিত কীগুলির সাথে ফাইল, যার মধ্যে Redis টোকেনটি আপনার Upstash কনসোলে পাওয়া যাবে, এখানে আপনার অ্যাকাউন্টের অধীনে প্রতিলিপি API টোকেন, এবং আপনার সাইটের URL যেখানেই আপনি এটি স্থাপন করবেন, তাই এই ক্ষেত্রে এটি হবে Vercel স্থাপনার শেষ পয়েন্ট৷

SITE_URL=https://your-project-url.vercel.app
 
REPLICATE_API_TOKEN=
 
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=

ফ্রন্টএন্ড ফর্ম সেট আপ করা হচ্ছে

শুরু করার জন্য, আমাদের একটি ফর্মের প্রয়োজন হবে যা ফর্ম, পোলিং এবং সম্পূর্ণ চিত্রগুলি প্রদর্শন করে৷

ইমেজ ফর্ম তৈরি পুনরুদ্ধার করুন

ফাইল:pages/index.tsx

import { MouseEvent, RefObject, useRef, useState } from "react";
import Head from "next/head";
 
import useInterval from "../hooks/useInterval";
 
export default function Home() {
 const [restoring, setRestoring] = useState<boolean>(false);
 const [messageId, setMessageId] = useState<string | null>(null);
 const [prediction, setPrediction] = useState<any>({});
 const [outputImageUrl, setOutputImageUrl] = useState<string | null>(null);
 const imageUrlRef: RefObject<HTMLInputElement> = useRef(null);
 const hrRef: RefObject<HTMLInputElement> = useRef(null);
 const scratchRef: RefObject<HTMLInputElement> = useRef(null);
 
 useInterval(
 async () => {
 await fetch(`/api/poll?id=${messageId}`)
 .then((res: any) => res.json())
 .then((data: any) => {
 if (!data.output) {
 return;
 }
 
 setRestoring(false);
 setMessageId(null);
 setOutputImageUrl(data.output);
 })
 .catch((err: any) => console.error(err));
 },
 messageId ? 1000 : null,
 );
 
 async function restoreImage(e: any) {
 e.preventDefault();
 
 setRestoring(true);
 
 await fetch("/api/create", {
 method: "POST",
 body: JSON.stringify({
 image_url: imageUrlRef.current?.value,
 is_hr: hrRef.current?.value,
 has_scratches: scratchRef.current?.value,
 }),
 headers: { "Content-Type": "application/json" },
 })
 .then((res: Response) => res.json())
 .then((data: any) => {
 setMessageId(data.data.id);
 setPrediction(data.data);
 })
 .catch((err: Error) => console.error(err));
 }
 
 async function cancel(e: MouseEvent<HTMLButtonElement>) {
 e.preventDefault();
 
 await fetch("/api/cancel", {
 method: "POST",
 headers: { "Content-Type": "application/json" },
 body: JSON.stringify({ cancel_url: prediction.urls.cancel }),
 })
 .then((res: Response) => res.json())
 .then((data: any) => {
 setMessageId(null);
 setPrediction({});
 setRestoring(false);
 })
 .catch((err: Error) => console.error(err));
 }
 
 return (
 <>
 <Head>
 <title>PhotoRescue</title>
 <meta
 name="description"
 content="A simple Next.js application that utilizes Replicate to restore old photos."
 />
 <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">PhotoRescue</h1>
 
 <p className="mt-4">Restore your old photos to their former glory.</p>
 
 {outputImageUrl && (
 <div className="flex flex-col items-center justify-center">
 <img
 src={outputImageUrl}
 alt="Restored Image"
 className="mt-8 h-auto w-72"
 />
 
 <button
 type="button"
 onClick={() => setOutputImageUrl(null)}
 className="mt-8 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"
 >
 Start Again
 </button>
 </div>
 )}
 
 {!outputImageUrl && (
 <form
 onSubmit={restoreImage}
 className="mt-10 flex w-full max-w-lg flex-col items-center"
 >
 <div className="w-full space-y-4">
 <div>
 <label htmlFor="image_url" className="text-sm font-semibold">
 Image URL
 </label>
 <input
 name="image_url"
 id="image_url"
 type="text"
 defaultValue="https://replicate.delivery/mgxm/b033ff07-1d2e-4768-a137-6c16b5ed4bed/d_1.png"
 placeholder="https://example.com/image.png"
 className="mt-0.5 block w-full rounded-md border border-gray-300 p-2 shadow-sm focus:border-gray-500 focus:ring-gray-500"
 ref={imageUrlRef}
 required
 />
 </div>
 <div className="max-w-lg space-y-4">
 <div className="relative flex items-start">
 <div className="flex h-5 items-center">
 <input
 name="is_hr"
 id="is_hr"
 type="checkbox"
 className="h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-gray-500"
 ref={hrRef}
 />
 </div>
 <div className="ml-3 text-sm">
 <label
 htmlFor="is_hr"
 className="font-medium text-gray-900"
 >
 Is High Resolution?
 </label>
 <p className="text-gray-500">
 Check this if the input image is a high resolution
 photo.
 </p>
 </div>
 </div>
 <div className="relative flex items-start">
 <div className="flex h-5 items-center">
 <input
 name="is_scratched"
 id="is_scratched"
 type="checkbox"
 className="h-4 w-4 rounded border-gray-300 text-gray-900 focus:ring-gray-500"
 ref={scratchRef}
 defaultChecked={true}
 />
 </div>
 <div className="ml-3 text-sm">
 <label
 htmlFor="is_scratched"
 className="font-medium text-gray-900"
 >
 Has Scratches?
 </label>
 <p className="text-gray-500">
 Check this if the input image has visible scratches over
 it.
 </p>
 </div>
 </div>
 </div>
 </div>
 
 <div className="mt-6 flex gap-2">
 <button
 type="submit"
 disabled={restoring}
 className="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"
 >
 {restoring ? "Restoring..." : "Restore"}
 </button>
 
 {restoring && prediction && (
 <button
 type="button"
 onClick={cancel}
 className="inline-flex items-center rounded-full border border-gray-900 bg-white px-6 py-2.5 text-sm font-medium text-gray-900 shadow-sm hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-600 focus:ring-offset-2"
 >
 Cancel
 </button>
 )}
 </div>
 </form>
 )}
 </div>
 </main>
 </>
 );
}

ডিফল্টরূপে, এই উপাদানটি এমন একটি ফর্ম প্রদর্শন করে যা ব্যবহারকারীকে সেই চিত্রটির ইউআরএল প্রবেশ করার অনুমতি দেয় যা তারা পুনরুদ্ধার করতে চায়, এবং এটির পাশাপাশি যাওয়ার জন্য কয়েকটি বিকল্প যেমন চিত্রটি উচ্চ রেজোলিউশন কিনা, বা ছবিতে স্ক্র্যাচ আছে যা অপসারণ করতে হবে। একবার ব্যবহারকারী এই তথ্য পূরণ করে, এবং ফর্ম জমা দিলে, এটি একটি POST পাঠায় /api/create কে অনুরোধ করুন ফর্ম ডেটা সহ।

একবার এই অনুরোধটি এপিআই-এ পাঠানো হয়ে গেলে এবং ফেরত দেওয়া পূর্বাভাসের তথ্য সহ একটি প্রতিক্রিয়া পাওয়া গেলে, উপাদানটি একটি পোলিং স্টেটে প্রবেশ করে যা চেক করে একটি GET পাঠায় /api/poll কে অনুরোধ করুন ভবিষ্যদ্বাণীটি এখনও সম্পূর্ণ হয়েছে কিনা তা পরীক্ষা করার জন্য প্রতি সেকেন্ডে একবার। একবার একটি পোলিং অনুরোধ একটি সফল প্রতিক্রিয়া ফেরত দিলে, এটি নির্দেশ করে যে প্রতিলিপি আমাদের কলব্যাক এন্ডপয়েন্টে একটি অনুরোধ পাঠিয়েছে, আমরা এখন ভবিষ্যদ্বাণী আউটপুটে অ্যাক্সেস পাব৷

ভোটগ্রহণ চলাকালীন, ফর্মটি ভবিষ্যদ্বাণী বাতিল করার বিকল্প সহ একটি বোতাম প্রদর্শন করে। একবার চাপলে, এটি একটি পোস্ট পাঠায় /api/cancel এর জন্য অনুরোধ cancel_url দিয়ে ভবিষ্যদ্বাণী ডেটা থেকে যা আমরা প্রাথমিক সৃষ্টির সময় পেয়েছি।

পোলিং বাস্তবায়ন hooks/useInterval.ts-এ অবস্থিত হওয়ার জন্য একটি কাস্টম হুক ব্যবহার করে যা আমাদেরকে সহজে এবং নির্বিঘ্নে React-এর কম্পোনেন্ট লাইফস্টাইলের সাথে কাজ করতে দেয় এবং যেকোন প্রদত্ত রিঅ্যাক্ট কম্পোনেন্টের মধ্যে কলব্যাক সহ ব্যবধানগুলি পরিচালনা করার আরও সুবিধাজনক উপায় প্রদান করে। আপনি এখানে এই হুক সম্পর্কে আরও পড়তে পারেন এবং এখানে আপনি যদি এটি সম্পর্কে আরও এবং আরও বিশদে জানতে চান।

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;

API সেটআপ

কয়েকটি ফাইলের সমন্বয়ে গঠিত এপিআই সেটআপই আমাদের ভবিষ্যদ্বাণী তৈরি এবং বাতিল করতে দেয়, ভবিষ্যদ্বাণীগুলি কখন সম্পূর্ণ হয় তা পরীক্ষা করার জন্য পোল, সেইসাথে তাদের শেষের পূর্বাভাস সম্পূর্ণ হলে প্রতিলিপি ব্যবহার করবে এমন কলব্যাক নির্দিষ্ট করে৷

ইমেজ ভবিষ্যদ্বাণী তৈরি

ফাইল:pages/api/create.ts

import type { NextApiRequest, NextApiResponse } from "next";
 
import fetch, { Response } from "node-fetch";
 
import redis from "../../lib/redis";
 
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 { image_url, is_hr, has_scratches }: any = req.body;
 
 await fetch("https://api.replicate.com/v1/predictions", {
 method: "POST",
 headers: {
 Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`,
 "Content-Type": "application/json",
 },
 body: JSON.stringify({
 version:
 "c75db81db6cbd809d93cc3b7e7a088a351a3349c9fa02b6d393e35e0d51ba799",
 input: {
 image: image_url,
 HR: is_hr,
 with_scratch: has_scratches,
 },
 webhook_completed: `${process.env.SITE_URL}/api/callback`,
 }),
 })
 .then((res: Response) => res.json())
 .then(async (data: any) => {
 await redis.set(data.id, data);
 
 return res.status(202).json({ data: data });
 })
 .catch((error: Error) => {
 return res.status(500).json({ message: error.message });
 });
}

আমাদের API এন্ডপয়েন্ট তৈরি করার জন্য, আগত অনুরোধের পদ্ধতিটি একটি POST তা নিশ্চিত করতে আমরা প্রথমে একটি সাধারণ পরীক্ষা করি অনুরোধ, এবং না হলে আমরা একটি সহজ 400 প্রতিক্রিয়া ফেরত দেব। তারপরে আমরা একটি POST পাঠাতে এগিয়ে যাই আমাদের প্রতিলিপি API টোকেন দিয়ে প্রতিলিপি করার অনুরোধ। অনুরোধের মূল অংশে প্রদত্ত মডেল version এর পরামিতি রয়েছে যা নির্দেশ করে যে আমরা কোন মডেলে অনুরোধ পাঠাচ্ছি (এটি আপনি যে মডেলটি ব্যবহার করতে চান তার "API" ট্যাবের নিচে পাওয়া যায়)। আমরা ফ্রন্টএন্ডের ফর্ম থেকে ডেটা সহ মডেলের সাথে যুক্ত প্যারামিটারগুলিও পাস করি৷

একবার অনুরোধ পাঠানো হলে, আমরা ফেরত দেওয়া পূর্বাভাস id ব্যবহার করি এটি Redis-এ সঞ্চয় করতে, এবং পূর্বাভাস ডেটাকে ফ্রন্টএন্ডে ফেরত পাঠান যাতে Redis আইটেমটি পোলিং করার জন্য ব্যবহার করা হয় যতক্ষণ না এটি একটি সম্পূর্ণ ভবিষ্যদ্বাণী ধারণ করে৷

কলব্যাক

ফাইল: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 {
 await redis.set(body.id, body);
 
 return res.status(200).send(body);
 } catch (error) {
 return res.status(500).json({ error });
 }
}

কলব্যাক এন্ডপয়েন্ট হল যা প্রতিলিপি একটি POST পাঠাবে৷ একটি প্রদত্ত ভবিষ্যদ্বাণীর প্রক্রিয়াকরণ শেষ হয়েছে তা আমাদের জানানোর জন্য অনুরোধ করুন৷ যখন আমরা এই অনুরোধটি পাই, আমরা অনুরোধের বডি থেকে ভবিষ্যদ্বাণী ডেটা পুনরুদ্ধার করি এবং প্রদত্ত Redis আইটেমটি সম্পূর্ণ পূর্বাভাস ডেটা সহ আপডেট করি৷

পোলিং

ফাইল: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 বের করি অনুরোধ থেকে এবং তারপর সেই শনাক্তকারীর অধীনে Redis-এ সংরক্ষিত ডেটা পুনরুদ্ধার করার চেষ্টা করুন, এবং যদি কোনও ডেটা না পাওয়া যায়, আমরা একটি 404 প্রতিক্রিয়া প্রদান করি, কিন্তু যদি ডেটা থাকে, আমরা 200 প্রতিক্রিয়ার অংশ হিসাবে উল্লিখিত ডেটা ফিরিয়ে দিই৷

বাতিল করুন

ফাইল:pages/api/cancel.tsx

import type { NextApiRequest, NextApiResponse } from "next";
 
import fetch, { Response } from "node-fetch";
 
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 { cancel_url }: any = req.body;
 
 await fetch(cancel_url, {
 method: "POST",
 headers: {
 Authorization: `Token ${process.env.REPLICATE_API_TOKEN}`,
 "Content-Type": "application/json",
 },
 })
 .then((res: Response) => res.json())
 .then((data: any) => {
 return res.status(202).json({ data: data });
 })
 .catch((error: Error) => {
 return res.status(500).json({ message: error.message });
 });
}

ভবিষ্যদ্বাণী বাতিল করার জন্য API এন্ডপয়েন্ট যা শুরু করা হয়েছে তা সহজবোধ্য। আমরা সহজভাবে cancel_url বের করি যা ফ্রন্টএন্ড থেকে পাস করা হয় যা নিজেই ভবিষ্যদ্বাণী থেকে আসে যা একটি সৃষ্টির অনুরোধ জমা দেওয়ার সময় সংরক্ষণ করা হয়েছিল এবং আমরা কেবল একটি POST পাঠাই আমাদের প্রতিলিপি API টোকেনের পাশাপাশি সেই শেষপয়েন্টে অনুরোধ করুন।

Libs

আমাদের libs-এর জন্য, আমরা একটি Redis ক্লায়েন্ট তৈরি করব যা ট্র্যাকিংয়ে ব্যবহৃত হয়

ফাইল: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;

এই বস্তুটি অ্যাপ্লিকেশনের মধ্যে ব্যবহার করা হবে পোল করার সময় ডেটা সংরক্ষণ এবং পুনরুদ্ধার করার জন্য, যাতে আমরা জানতে পারি কখন প্রতিলিপি থেকে ওয়েবহুক সম্পূর্ণ হয়েছে৷

উপসংহার

প্রতিলিপিতে বিভিন্ন মডেল উপলব্ধ রয়েছে যা একটি API এর মাধ্যমে ব্যবহার করা যেতে পারে। Vercel এবং Upstash এর সাথে, মেশিন লার্নিং মডেলগুলি ব্যবহার করা এবং ব্যবহারযোগ্য ওয়েব অ্যাপ্লিকেশন স্থাপন করা আগের চেয়ে সহজ৷

আপনি যদি সম্পূর্ণ সংগ্রহস্থল দেখতে চান, আপনি এটি এখানে অ্যাক্সেস করতে পারেন।

আরো উন্নয়ন

এটি প্রতিলিপি সহ একটি বরং সাধারণ মডেল ব্যবহার করার একটি সহজ উদাহরণ। এপিআই-তে ফর্ম প্যারামিটার এবং ভার্সন পরিবর্তন করে, আপনি সহজেই অন্য মডেলে পরিবর্তন করতে পারবেন যতক্ষণ না আপনার প্রতিলিপি এপিআই টোকেন লিঙ্ক করা থাকে, আপনি উপলব্ধ মডেলগুলির যেকোনও ব্যবহার করতে পারবেন।

আপনি এখানে প্রতিলিপির উপলব্ধ সমস্ত মডেলগুলি অন্বেষণ করতে পারেন, এবং একবার আপনি একটিকে খুঁজে পেলে যার সাথে আপনি পরীক্ষা করতে চান, আপনি এটির ব্যবহার দেখার জন্য "API" ট্যাবে ক্লিক করতে পারেন৷ এখানে আপনি Python, cURL, Cog এবং Docker-এর জন্য বোতামগুলিও পাবেন, যা আপনাকে মডেলটি পরীক্ষা করার অনুমতি দেয়, তবে কোন প্যারামিটারগুলি প্রয়োজন এবং সেগুলি কীভাবে পাঠানো হয় তা জানার জন্যও এটি দরকারী৷


  1. IDCS এর সাথে EBS SSO ইন্টিগ্রেশন

  2. জাভা ওপেনসিভি লাইব্রেরি ব্যবহার করে কীভাবে আরজিবি ইমেজকে এইচএসভিতে রূপান্তর করবেন?

  3. কিভাবে Tkinter এ উইজেট দেখাবেন এবং লুকাবেন?

  4. Upstash এবং Node.js দিয়ে একটি রিয়েল-টাইম আর্টিকেল রেকমেন্ডেশন ইঞ্জিন তৈরি করুন