বাড়ি ফিরে আসার পথে - একটি ADK দ্বি-মুখী স্ট্রিমিং এজেন্ট তৈরি করা

১. মিশন

গল্প

আপনি এক অনাবিষ্কৃত অঞ্চলের নিস্তব্ধতায় ভেসে চলেছেন। একটি প্রচণ্ড সৌর স্পন্দন আপনার মহাকাশযানটিকে একটি ফাটলের মধ্যে দিয়ে ছিঁড়ে ফেলেছে, ফলে আপনি মহাবিশ্বের এমন এক প্রকোষ্ঠে আটকা পড়েছেন যার কোনো নক্ষত্র-মানচিত্রে অস্তিত্ব নেই।

কয়েক দিনের ক্লান্তিকর মেরামতের পর, অবশেষে আপনি আপনার পায়ের নিচে ইঞ্জিনের গুঞ্জন অনুভব করছেন। আপনার রকেটশিপটি ঠিক হয়ে গেছে। এমনকি আপনি মাদারশিপের সাথে একটি দূরপাল্লার সংযোগও স্থাপন করতে পেরেছেন। আপনি যাত্রার জন্য প্রস্তুত। আপনি বাড়ি ফেরার জন্য তৈরি। কিন্তু যেই মুহূর্তে আপনি জাম্প ড্রাইভ চালু করার প্রস্তুতি নিচ্ছেন, একটি বিপদ সংকেত স্ট্যাটিক ভেদ করে ভেসে আসে। আপনার সেন্সরগুলো "দ্য র‍্যাভাইন" -এ আটকে থাকা পাঁচটি ক্ষীণ তাপীয় সংকেত শনাক্ত করে—এটি একটি এবড়োখেবড়ো, মাধ্যাকর্ষণ-বিকৃত এলাকা যেখানে আপনার মূল জাহাজ কখনোই প্রবেশ করতে পারে না। এরা আপনার সহযাত্রী অভিযাত্রী, সেই একই ঝড়ের বেঁচে ফেরা মানুষ যা আপনাকে প্রায় কেড়ে নিয়েছিল। আপনি তাদের ফেলে যেতে পারেন না।

আপনি আপনার আলফা-ড্রোন রেসকিউ স্কাউটের দিকে ফিরলেন। এই ছোট, ক্ষিপ্র যানটিই একমাত্র যানবাহন যা গিরিখাতের সংকীর্ণ প্রাচীরের মধ্যে দিয়ে চলাচল করতে সক্ষম। কিন্তু একটি সমস্যা আছে: সৌর স্পন্দন এর মূল কার্যপ্রণালীতে একটি সম্পূর্ণ 'সিস্টেম রিসেট' ঘটিয়েছে। স্কাউটের নিয়ন্ত্রণ ব্যবস্থাগুলো অচল হয়ে পড়েছে। এটি চালু আছে, কিন্তু এর ভেতরের কম্পিউটারটি একটি ফাঁকা স্লেটের মতো, যা পাইলটের হাতে দেওয়া নির্দেশ বা উড্ডয়নের পথ বুঝতে পারছে না।

চ্যালেঞ্জ

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

সংযোগটি দৃঢ় করতে, আপনাকে স্কাউটের অপটিক্যাল সেন্সরগুলোর সামনে সিনক্রোনাইজেশন প্রোটোকলটি সম্পাদন করতে হবে। এআই এজেন্টকে একটি সুনির্দিষ্ট, রিয়েল-টাইম হ্যান্ডশেকের মাধ্যমে আপনার জৈবিক স্বাক্ষর শনাক্ত করতে হবে।

মিশন আলফা

আপনার মিশনের উদ্দেশ্যসমূহ:

  1. নিউরাল কোরে ছাপ ফেলুন: মাল্টিমোডাল ইনপুট শনাক্ত করতে সক্ষম একটি ADK এজেন্ট সংজ্ঞায়িত করুন।
  2. সংযোগ স্থাপন করুন: স্কাউট থেকে এআই-তে ভিজ্যুয়াল ডেটা স্ট্রিম করার জন্য একটি দ্বি-মুখী ওয়েবসকেট পাইপলাইন তৈরি করুন।
  3. হ্যান্ডশেক শুরু করুন: সেন্সরের সামনে দাঁড়ান এবং ক্রমানুসারে ১ থেকে ৫ পর্যন্ত আঙুলগুলো দেখিয়ে হ্যান্ডশেক সম্পন্ন করুন।

সফল হলে, "বায়োমেট্রিক সিঙ্ক" সক্রিয় হবে। এআই নিউরাল লিঙ্কটি লক করে দেবে, যা আপনাকে স্কাউটটি চালু করতে এবং সেই জীবিতদের বাড়ি ফিরিয়ে আনতে সম্পূর্ণ ম্যানুয়াল নিয়ন্ত্রণ দেবে।

আপনি যা তৈরি করবেন

সংক্ষিপ্ত বিবরণ

আপনি একটি "বায়োমেট্রিক নিউরাল সিঙ্ক" অ্যাপ্লিকেশন তৈরি করবেন, যা একটি উদ্ধারকারী ড্রোনের নিয়ন্ত্রণ ইন্টারফেস হিসেবে কাজ করে এমন একটি রিয়েল-টাইম, এআই-চালিত সিস্টেম। এই সিস্টেমটিতে রয়েছে:

  • একটি রিয়্যাক্ট ফ্রন্টএন্ড: আপনার জাহাজের "ককপিট", যা আপনার ওয়েবক্যাম থেকে লাইভ ভিডিও এবং মাইক্রোফোন থেকে অডিও ধারণ করে।
  • একটি পাইথন ব্যাকএন্ড: ফাস্টএপিআই (FastAPI) দিয়ে নির্মিত একটি উচ্চ-কর্মক্ষমতাসম্পন্ন সার্ভার, যা এলএলএম (LLM)-এর লজিক এবং স্টেট পরিচালনা করতে গুগলের এজেন্ট ডেভেলপমেন্ট কিট (ADK) ব্যবহার করে।
  • একটি মাল্টিমোডাল এআই এজেন্ট: এই কার্যক্রমের 'মস্তিষ্ক', যা google-genai এসডিকে-এর মাধ্যমে জেমিনি লাইভ এপিআই ব্যবহার করে একই সাথে ভিডিও এবং অডিও স্ট্রিম প্রক্রিয়াকরণ ও অনুধাবন করে।
  • একটি দ্বি-মুখী ওয়েবসকেট পাইপলাইন: এটি সেই "স্নায়ুতন্ত্র" যা ফ্রন্টএন্ড এবং এআই-এর মধ্যে একটি স্থায়ী ও স্বল্প-বিলম্বের সংযোগ তৈরি করে, যার ফলে রিয়েল-টাইম মিথস্ক্রিয়া সম্ভব হয়।

আপনি যা শিখবেন

প্রযুক্তি / ধারণা

বর্ণনা

ব্যাকএন্ড এআই এজেন্ট

পাইথন এবং ফাস্টএপিআই (FastAPI) ব্যবহার করে একটি স্টেটফুল এআই এজেন্ট তৈরি করুন। নির্দেশাবলী ও মেমরি ব্যবস্থাপনার জন্য গুগলের এডিকে (এজেন্ট ডেভেলপমেন্ট কিট) এবং জেমিনি মডেলের সাথে যোগাযোগের জন্য google-genai এসডিকে ব্যবহার করুন।

ফ্রন্টএন্ড UI

সরাসরি ব্রাউজার থেকে লাইভ ভিডিও এবং অডিও ক্যাপচার ও স্ট্রিম করার জন্য React ব্যবহার করে একটি ডায়নামিক ইউজার ইন্টারফেস তৈরি করুন।

রিয়েল-টাইম যোগাযোগ

ফুল-ডুপ্লেক্স ও স্বল্প-বিলম্বের যোগাযোগের জন্য একটি ওয়েবসকেট পাইপলাইন বাস্তবায়ন করুন, যা ব্যবহারকারী এবং এআই-কে একই সাথে মিথস্ক্রিয়া করতে সক্ষম করবে।

মাল্টিমোডাল এআই

একই সাথে একাধিক ভিডিও এবং অডিও স্ট্রিম প্রসেস ও বোঝার জন্য জেমিনি লাইভ এপিআই ব্যবহার করুন, যা এআই-কে একই সময়ে 'দেখতে' এবং 'শুনতে' সক্ষম করে তোলে।

টুল কলিং

ভিজ্যুয়াল ট্রিগারের প্রতিক্রিয়ায় নির্দিষ্ট পাইথন ফাংশন সম্পাদন করার জন্য এআই-কে সক্ষম করুন, যা মডেলের বুদ্ধিমত্তা এবং বাস্তব জগতের কার্যকলাপের মধ্যে ব্যবধান কমিয়ে আনবে।

ফুল-স্ট্যাক ডেপ্লয়মেন্ট

সম্পূর্ণ অ্যাপ্লিকেশনটিকে (রিঅ্যাক্ট ফ্রন্টএন্ড এবং পাইথন ব্যাকএন্ড) ডকারের মাধ্যমে কন্টেইনারাইজ করুন এবং গুগল ক্লাউড রান- এ একটি স্কেলেবল, সার্ভারলেস সার্ভিস হিসেবে ডেপ্লয় করুন।

২. আপনার পরিবেশ প্রস্তুত করুন

ক্লাউড শেল অ্যাক্সেস করুন

প্রথমে, আমরা ক্লাউড শেল খুলব, যেটি হলো একটি ব্রাউজার-ভিত্তিক টার্মিনাল এবং এতে গুগল ক্লাউড এসডিকে ও অন্যান্য প্রয়োজনীয় টুল আগে থেকেই ইনস্টল করা থাকে।

👉Google Cloud কনসোলের উপরে থাকা Activate Cloud Shell-এ ক্লিক করুন (এটি Cloud Shell পেনের উপরে থাকা টার্মিনাল আকৃতির আইকন), মেঘ-খোলস.png

👉'ওপেন এডিটর' বোতামটিতে (এটি দেখতে পেন্সিলসহ একটি খোলা ফোল্ডারের মতো) ক্লিক করুন। এটি উইন্ডোতে ক্লাউড শেল কোড এডিটর খুলে দেবে। আপনি বাম দিকে একটি ফাইল এক্সপ্লোরার দেখতে পাবেন। open-editor.png

👉ক্লাউড IDE-তে টার্মিনালটি খুলুন,

03-05-new-terminal.png

👉💻 টার্মিনালে, নিম্নলিখিত কমান্ডটি ব্যবহার করে যাচাই করুন যে আপনি ইতিমধ্যেই প্রমাণীকৃত এবং প্রজেক্টটি আপনার প্রজেক্ট আইডিতে সেট করা আছে:

gcloud auth list

আপনার অ্যাকাউন্টটি (ACTIVE) হিসেবে তালিকাভুক্ত দেখতে পাবেন।

পূর্বশর্ত

ℹ️ লেভেল ০ ঐচ্ছিক (তবে সুপারিশকৃত)

আপনি লেভেল ০ ছাড়াও এই মিশনটি সম্পন্ন করতে পারেন, কিন্তু প্রথমে এটি শেষ করলে আরও বেশি বাস্তবসম্মত অভিজ্ঞতা পাওয়া যায়, যার ফলে আপনি অগ্রগতির সাথে সাথে গ্লোবাল ম্যাপে আপনার বীকনটি জ্বলতে দেখতে পাবেন।

প্রকল্পের পরিবেশ সেটআপ করুন

আপনার টার্মিনালে ফিরে এসে, সক্রিয় প্রজেক্ট সেট করে এবং প্রয়োজনীয় গুগল ক্লাউড পরিষেবাগুলি (ক্লাউড রান, ভার্টেক্স এআই, ইত্যাদি) সক্রিয় করে কনফিগারেশনটি চূড়ান্ত করুন।

👉💻 আপনার টার্মিনালে প্রজেক্ট আইডি সেট করুন:

gcloud config set project $(cat ~/project_id.txt) --quiet

👉💻 প্রয়োজনীয় পরিষেবাগুলি সক্রিয় করুন:

gcloud services enable  compute.googleapis.com \
                        artifactregistry.googleapis.com \
                        run.googleapis.com \
                        cloudbuild.googleapis.com \
                        iam.googleapis.com \
                        aiplatform.googleapis.com

নির্ভরতা ইনস্টল করুন

👉💻 Level-এ যান এবং প্রয়োজনীয় Python প্যাকেজগুলো ইনস্টল করুন:

cd $HOME/way-back-home/level_3
uv sync

মূল নির্ভরশীলতাগুলো হলো:

প্যাকেজ

উদ্দেশ্য

fastapi

স্যাটেলাইট স্টেশন এবং এসএসই স্ট্রিমিংয়ের জন্য উচ্চ-কর্মক্ষমতাসম্পন্ন ওয়েব ফ্রেমওয়ার্ক

uvicorn

FastAPI অ্যাপ্লিকেশনটি চালানোর জন্য ASGI সার্ভার প্রয়োজন।

google-adk

ফর্মেশন এজেন্ট তৈরি করতে ব্যবহৃত এজেন্ট ডেভেলপমেন্ট কিট

google-genai

জেমিনি মডেল অ্যাক্সেস করার জন্য নেটিভ ক্লায়েন্ট

websockets

রিয়েল-টাইম দ্বিমুখী যোগাযোগের জন্য সমর্থন

python-dotenv

পরিবেশ ভেরিয়েবল এবং কনফিগারেশন গোপনীয়তা পরিচালনা করে

সেটআপ যাচাই করুন

কোড শুরু করার আগে, চলুন নিশ্চিত করে নিই যে সবকিছু ঠিকঠাক আছে। আপনার গুগল ক্লাউড প্রজেক্ট, এপিআই (API), এবং পাইথন ডিপেন্ডেন্সিগুলো নিরীক্ষা করতে ভেরিফিকেশন স্ক্রিপ্টটি চালান।

👉💻 ভেরিফিকেশন স্ক্রিপ্টটি চালান:

cd $HOME/way-back-home/level_3/scripts
chmod +x verify_setup.sh
. verify_setup.sh

👀 আপনি পরপর কয়েকটি সবুজ টিক চিহ্ন (✅) দেখতে পাবেন।

  • যদি আপনি লাল ক্রস (❌) চিহ্ন দেখতে পান, তাহলে আউটপুটে দেওয়া সমাধান কমান্ডগুলো অনুসরণ করুন (যেমন, gcloud services enable ... অথবা pip install ... )।
  • দ্রষ্টব্য: .env জন্য একটি হলুদ সতর্কবার্তা আপাতত গ্রহণযোগ্য; আমরা পরবর্তী ধাপে ফাইলটি তৈরি করব।
🚀 Verifying Mission Alpha (Level 3) Infrastructure...

✅ Google Cloud Project: xxxxxx
✅ Cloud APIs: Active
✅ Python Environment: Ready

🎉 SYSTEMS ONLINE. READY FOR MISSION.

৩. যোগাযোগ-লিঙ্ক (ওয়েবসকেট) ক্রমাঙ্কন করা

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

ফুল-ডুপ্লেক্স বনাম হাফ-ডুপ্লেক্স

নিউরাল সিঙ্কের জন্য আমাদের কেন এটির প্রয়োজন, তা বুঝতে হলে আপনাকে ডেটার প্রবাহ বুঝতে হবে:

  • হাফ-ডুপ্লেক্স (স্ট্যান্ডার্ড HTTP): এটি ওয়াকি-টকির মতো। একজন কথা বলে "ওভার" বললে, অপরজন কথা বলতে পারেন। একই সাথে শোনা এবং কথা বলা যায় না।
  • ফুল-ডুপ্লেক্স (ওয়েবসকেট): ঠিক যেন মুখোমুখি কথোপকথন। ডেটা একই সাথে উভয় দিকে প্রবাহিত হয়। যখন আপনার ব্রাউজার এআই-এর কাছে ভিডিও ফ্রেম এবং অডিও স্যাম্পল পাঠায়, ঠিক একই সময়ে এআই আপনার কাছে ভয়েস রেসপন্স এবং টুল কমান্ড পাঠাতে পারে।

জেমিনি লাইভের কেন ফুল-ডুপ্লেক্স প্রয়োজন: জেমিনি লাইভ এপিআই "ইন্টারাপশন" বা বাধা দেওয়ার জন্য ডিজাইন করা হয়েছে। কল্পনা করুন, আপনি আঙুলের একটি সিকোয়েন্স দেখাচ্ছেন এবং এআই (AI) দেখল যে আপনি এটি ভুলভাবে করছেন। একটি সাধারণ HTTP সেটআপে, এআই-কে আপনাকে থামতে বলার আগে আপনার ডেটা পাঠানো শেষ হওয়া পর্যন্ত অপেক্ষা করতে হবে। ওয়েবসকেটস (WebSockets) ব্যবহার করে, এআই ফ্রেম ১-এই একটি ভুল দেখতে পায় এবং একটি "ইন্টারাপ্ট" সিগন্যাল পাঠাতে পারে, যা আপনার ককপিটে এসে পৌঁছায় যখন আপনি ফ্রেম ২-এর জন্য তখনও আপনার হাত নাড়াচ্ছেন।

ডুপ্লেক্স

ওয়েবসকেট কী?

একটি সাধারণ গ্যালাকটিক ট্রান্সমিশনে (HTTP), আপনি একটি অনুরোধ পাঠান এবং উত্তরের জন্য অপেক্ষা করেন—যেমন একটি পোস্টকার্ড পাঠানো। নিউরাল সিঙ্কের জন্য, পোস্টকার্ড খুব ধীরগতির। আমাদের একটি 'সচল তার' প্রয়োজন।

ওয়েবসকেটস একটি সাধারণ ওয়েব অনুরোধ (HTTP) হিসেবে শুরু হয়, কিন্তু পরে এটি ভিন্ন কিছুতে "আপগ্রেড" হয়।

  1. অনুরোধ: আপনার ব্রাউজার একটি বিশেষ হেডার সহ সার্ভারে একটি স্ট্যান্ডার্ড HTTP অনুরোধ পাঠায়: Upgrade: websocket । এর মাধ্যমে মূলত বলা হচ্ছে, "আমি পোস্টকার্ড পাঠানো বন্ধ করে একটি সরাসরি ফোন কল শুরু করতে চাই।"
  2. প্রতিক্রিয়া: যদি এআই এজেন্ট (সার্ভার) এটি সমর্থন করে, তবে এটি একটি HTTP 101 Switching Protocols প্রতিক্রিয়া ফেরত পাঠায়।
  3. রূপান্তর: এই মুহূর্তে, HTTP সংযোগটি WebSocket প্রোটোকল দ্বারা প্রতিস্থাপিত হয়, কিন্তু অন্তর্নিহিত TCP/IP সকেটটি খোলা থাকে। যোগাযোগের নিয়ম তাৎক্ষণিকভাবে 'অনুরোধ/প্রতিক্রিয়া' থেকে 'ফুল-ডুপ্লেক্স স্ট্রিমিং'-এ পরিবর্তিত হয়।

ওয়েবসকেট হুক বাস্তবায়ন করুন

ডেটা কীভাবে প্রবাহিত হয় তা বোঝার জন্য চলুন টার্মিনাল ব্লকটি পরীক্ষা করে দেখি।

👀 $HOME/way-back-home/level_3/frontend/src/useGeminiSocket.js খুলুন। আপনি দেখবেন, স্ট্যান্ডার্ড ওয়েবসকেট লাইফসাইকেল ইভেন্ট হ্যান্ডলারগুলো আগে থেকেই সেট আপ করা আছে। এটিই আমাদের যোগাযোগ ব্যবস্থার মূল কাঠামো:

const connect = useCallback(() => {
        if (ws.current?.readyState === WebSocket.OPEN) return;

        ws.current = new WebSocket(url);

        ws.current.onopen = () => {
            console.log('Connected to Gemini Socket');
            setStatus('CONNECTED');
        };

        ws.current.onclose = () => {
            console.log('Disconnected from Gemini Socket');
            setStatus('DISCONNECTED');
            stopStream();
        };

        ws.current.onerror = (err) => {
            console.error('Socket error:', err);
            setStatus('ERROR');
        };

        ws.current.onmessage = async (event) => {
            try {
//#REPLACE-HANDLE-MSG
            } catch (e) {
                console.error('Failed to parse message', e, event.data.slice(0, 100));
            }
        };
    }, [url]);

onMessage হ্যান্ডলার

ws.current.onmessage ব্লকটির উপর মনোযোগ দিন। এটি হলো রিসিভার। প্রতিবার যখন এজেন্ট 'চিন্তা' করে বা 'কথা' বলে, তখন এখানে একটি ডেটা প্যাকেট এসে পৌঁছায়। বর্তমানে, এটি কিছুই করে না—এটি প্যাকেটটি গ্রহণ করে এবং ( //#REPLACE-HANDLE-MSG প্লেসহোল্ডারের মাধ্যমে) তা ফেলে দেয়।

আমাদের এই শূন্যস্থান এমন যুক্তি দিয়ে পূরণ করতে হবে যা নিম্নলিখিত বিষয়গুলোর মধ্যে পার্থক্য করতে পারে:

  • টুল কল (ফাংশনকল): এআই দ্বারা আপনার হাতের ইশারা শনাক্তকরণ ("সিঙ্ক")।
  • অডিও ডেটা (ইনলাইন ডেটা): আপনার প্রতি এআই-এর কণ্ঠস্বরের প্রতিক্রিয়া।

👉✏️ এখন, একই $HOME/way-back-home/level_3/frontend/src/useGeminiSocket.js ফাইলে, আগত স্ট্রিমটি পরিচালনা করার জন্য //#REPLACE-HANDLE-MSG নিচের লজিক দিয়ে প্রতিস্থাপন করুন:

                // console.log("Raw WS Frame:", event.data.slice(0, 200)); 
                const msg = JSON.parse(event.data);

                // Detect mock server identification flag
                if (msg.mock === true) {
                    setIsMock(true);
                    return;
                }

                // Helper to extract parts from various possible event structures
                let parts = [];
                if (msg.serverContent?.modelTurn?.parts) {
                    parts = msg.serverContent.modelTurn.parts;
                } else if (msg.content?.parts) {
                    parts = msg.content.parts;
                }

                if (parts.length > 0) {
                    // console.log(`[useGeminiSocket] Processing ${parts.length} parts`);
                    parts.forEach(part => {
                        // Handle Tool Calls
                        if (part.functionCall) {
                            console.log('Tool Call Detected:', part.functionCall);
                            if (part.functionCall.name === 'report_digit') {
                                const count = parseInt(part.functionCall.args.count, 10);
                                setLastMessage({ type: 'DIGIT_DETECTED', value: count });
                            }
                        }

                        // Handle Audio (inlineData)
                        if (part.inlineData && part.inlineData.data) {
                            console.log(`[useGeminiSocket] Found inlineData: ${part.inlineData.data.length} chars`);
                            // Resume context if needed (autoplay policy)
                            audioStreamer.current.resume();
                            audioStreamer.current.addPCM16(part.inlineData.data);
                        }
                    });
                }

প্রেরণের জন্য অডিও এবং ভিডিও কীভাবে ডেটাতে রূপান্তরিত হয়

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

অডিও ডেটা রূপান্তর

অডিও ক্যাপচার

অ্যানালগ অডিওকে প্রেরণযোগ্য ডিজিটাল ডেটাতে রূপান্তর করার প্রক্রিয়াটি একটি মাইক্রোফোন ব্যবহার করে শব্দ তরঙ্গ ধারণ করার মাধ্যমে শুরু হয়। এরপর এই কাঁচা অডিও ব্রাউজারের ওয়েব অডিও এপিআই (Web Audio API)-এর মাধ্যমে প্রক্রিয়াজাত করা হয়। যেহেতু এই কাঁচা ডেটা একটি বাইনারি ফরম্যাটে থাকে, তাই এটি JSON-এর মতো টেক্সট-ভিত্তিক ট্রান্সমিশন ফরম্যাটের সাথে সরাসরি সামঞ্জস্যপূর্ণ নয়। এই সমস্যা সমাধানের জন্য, অডিওর প্রতিটি অংশকে একটি Base64 স্ট্রিং-এ এনকোড করা হয়। Base64 হলো একটি পদ্ধতি যা বাইনারি ডেটাকে একটি ASCII স্ট্রিং ফরম্যাটে উপস্থাপন করে, যা প্রেরণের সময় এর অখণ্ডতা নিশ্চিত করে।

এই এনকোড করা স্ট্রিংটি এরপর একটি JSON অবজেক্টের মধ্যে এমবেড করা হয়। এই অবজেক্টটি ডেটার জন্য একটি কাঠামোগত ফরম্যাট প্রদান করে, যাতে সাধারণত এটিকে অডিও হিসেবে শনাক্ত করার জন্য একটি 'টাইপ' ফিল্ড এবং অডিওর স্যাম্পল রেটের মতো মেটাডেটা অন্তর্ভুক্ত থাকে। এরপর সম্পূর্ণ JSON অবজেক্টটিকে একটি স্ট্রিং-এ সিরিয়ালাইজ করে একটি ওয়েবসকেট সংযোগের মাধ্যমে পাঠানো হয়। এই পদ্ধতিটি নিশ্চিত করে যে অডিওটি একটি সুসংগঠিত এবং সহজে পার্সযোগ্য পদ্ধতিতে প্রেরিত হয়।

ভিডিও ডেটা রূপান্তর

ভিডিও ক্যাপচার

ফ্রেম-ক্যাপচার কৌশলের মাধ্যমে ভিডিও প্রেরণ করা হয়। একটানা ভিডিও স্ট্রিম পাঠানোর পরিবর্তে, একটি পুনরাবৃত্তিমূলক লুপ নির্দিষ্ট বিরতিতে, যেমন প্রতি সেকেন্ডে দুটি ফ্রেমে, লাইভ ভিডিও ফিড থেকে স্থির চিত্র ধারণ করে। এটি একটি HTML ভিডিও এলিমেন্ট থেকে বর্তমান ফ্রেমটিকে একটি লুকানো ক্যানভাস এলিমেন্টের উপর অঙ্কন করার মাধ্যমে সম্পন্ন করা হয়।

এরপর ক্যানভাসের toDataURL মেথডটি ব্যবহার করে এই ক্যাপচার করা ছবিটিকে একটি Base64-এনকোডেড JPEG স্ট্রিং-এ রূপান্তর করা হয়। এই মেথডটিতে ছবির কোয়ালিটি নির্দিষ্ট করার একটি অপশন থাকে, যা পারফরম্যান্স অপ্টিমাইজ করার জন্য ছবির গুণমান এবং ফাইলের আকারের মধ্যে একটি ভারসাম্য রক্ষা করতে সাহায্য করে। অডিও ডেটার মতোই, এই Base64 স্ট্রিংটিকে এরপর একটি JSON অবজেক্টে রাখা হয়। এই অবজেক্টটিকে সাধারণত 'image' "type" দিয়ে লেবেল করা হয় এবং এতে mimeType অন্তর্ভুক্ত থাকে, যেমন 'image/jpeg'। এরপর এই JSON প্যাকেটটিকে একটি স্ট্রিং-এ রূপান্তর করে WebSocket-এর মাধ্যমে পাঠানো হয়, যা প্রাপককে ছবিগুলোর ক্রম প্রদর্শন করে ভিডিওটি পুনর্গঠন করার সুযোগ দেয়।

👉✏️ ব্যবহারকারীর ইনপুট ক্যাপচার করতে, একই $HOME/way-back-home/level_3/frontend/src/useGeminiSocket.js ফাইলে //#CAPTURE AUDIO and VIDEO নিম্নলিখিত দিয়ে প্রতিস্থাপন করুন:

            // 1. Start Video Stream
            const stream = await navigator.mediaDevices.getUserMedia({ video: true });
            videoElement.srcObject = stream;
            streamRef.current = stream;
            await videoElement.play();

            // 2. Start Audio Recording (Microphone)
            try {
                let packetCount = 0;
                await audioRecorder.current.start((base64Audio) => {
                    if (ws.current?.readyState === WebSocket.OPEN) {
                        packetCount++;
                        if (packetCount % 50 === 0) console.log(`[useGeminiSocket] Sending Audio Packet #${packetCount}, size: ${base64Audio.length}`);
                        ws.current.send(JSON.stringify({
                            type: 'audio',
                            data: base64Audio,
                            sampleRate: 16000
                        }));
                    } else {
                        if (packetCount % 50 === 0) console.warn('[useGeminiSocket] WS not OPEN, cannot send audio');
                    }
                });
                console.log("Microphone recording started");
            } catch (authErr) {
                console.error("Microphone access denied or error:", authErr);
            }

            // 3. Setup Video Frame Capture loop
            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            const width = 640;
            const height = 480;
            canvas.width = width;
            canvas.height = height;

            intervalRef.current = setInterval(() => {
                if (ws.current?.readyState === WebSocket.OPEN) {
                    ctx.drawImage(videoElement, 0, 0, width, height);
                    const base64 = canvas.toDataURL('image/jpeg', 0.6).split(',')[1];
                    // ADK format: { type: "image", data: base64, mimeType: "image/jpeg" }
                    ws.current.send(JSON.stringify({
                        type: 'image',
                        data: base64,
                        mimeType: 'image/jpeg'
                    }));
                }
            }, 500); // 2 FPS

একবার সংরক্ষণ করা হলে, ককপিটটি এজেন্টের ডিজিটাল সংকেতগুলোকে ভিজ্যুয়াল ড্যাশবোর্ড আপডেট এবং অডিওতে রূপান্তর করার জন্য প্রস্তুত থাকবে।

ডায়াগনস্টিক চেক (লুপব্যাক টেস্ট)

আপনার ককপিট এখন লাইভ। প্রতি ৫০০ মিলিসেকেন্ডে আপনার চারপাশের একটি ভিজ্যুয়াল 'প্যাকেট' পাঠানো হচ্ছে। জেমিনির সাথে সংযোগ করার আগে, আমাদের অবশ্যই যাচাই করতে হবে যে আপনার জাহাজের ট্রান্সমিটারটি কাজ করছে কিনা। আমরা একটি স্থানীয় ডায়াগনস্টিক সার্ভার ব্যবহার করে একটি 'লুপব্যাক টেস্ট' চালাব।

মক সার্ভার

👉💻 প্রথমে, আপনার টার্মিনাল থেকে ককপিট ইন্টারফেসটি তৈরি করুন:

cd $HOME/way-back-home/level_3/frontend
npm install
npm run build

👉💻 এরপর, মক সার্ভারটি চালু করুন:

cd $HOME/way-back-home/level_3
uv run mock/mock_server.py

👉 টেস্ট প্রোটোকলটি কার্যকর করুন:

  1. প্রিভিউ খুলুন: ক্লাউড শেল টুলবারে থাকা ওয়েব প্রিভিউ আইকনে ক্লিক করুন। ‘Change port’ নির্বাচন করে এটিকে 8080- তে সেট করুন এবং ‘Change and Preview’-তে ক্লিক করুন। একটি নতুন ব্রাউজার ট্যাব খুলবে যেখানে আপনার ককপিট ইন্টারফেস দেখা যাবে। ওয়েব-প্রিভিউ
  2. অত্যন্ত গুরুত্বপূর্ণ: অনুরোধ করা হলে, আপনাকে অবশ্যই ব্রাউজারকে আপনার ক্যামেরা এবং মাইক্রোফোন ব্যবহারের অনুমতি দিতে হবে। এই তথ্যগুলো না দিলে নিউরাল সিঙ্ক শুরু হতে পারবে না।
  3. UI-তে থাকা 'INITIATE NEURAL SYNC' বাটনটিতে ক্লিক করুন।

👀 স্ট্যাটাস সূচকগুলো যাচাই করুন:

  • চাক্ষুষ যাচাই: আপনার ব্রাউজার কনসোল খুলুন। আপনি উপরের ডানদিকে NEURAL SYNC INITIALIZED দেখতে পাবেন।
  • অডিও চেক: যদি আপনার দ্বি-মুখী অডিও পাইপলাইন সম্পূর্ণরূপে চালু থাকে, তাহলে আপনি একটি সিমুলেটেড ভয়েসের নিশ্চিতকরণ শুনতে পাবেন: " সিস্টেম সংযুক্ত! " মক-ফলাফল

"সিস্টেম সংযুক্ত!" অডিও নিশ্চিতকরণটি শোনার পর, পরীক্ষাটি সফল হবে। ট্যাবটি বন্ধ করুন। আসল এআই-এর জন্য জায়গা করে দিতে এখন আমাদের ফ্রিকোয়েন্সিটি খালি করতে হবে।

👉💻 মক সার্ভার এবং ফ্রন্টএন্ড উভয়ের টার্মিনালে Ctrl+C চাপুন। UI চালু থাকা ব্রাউজার ট্যাবটি বন্ধ করুন।

৪. মাল্টিমোডাল এজেন্ট

রেসকিউ স্কাউটটি চালু আছে, কিন্তু এর 'মন' শূন্য। আপনি এখন সংযোগ করলে, এটি শুধু আপনার দিকে তাকিয়ে থাকবে। 'আঙুল' কী, তা এটি জানে না। জীবিতদের বাঁচাতে হলে, আপনাকে স্কাউটের কোরে বায়োমেট্রিক নিউরাল প্রোটোকলটি স্থাপন করতে হবে।

প্রচলিত এজেন্ট একাধিক অনুবাদকের একটি শৃঙ্খলের মতো কাজ করে। আপনি যদি কোনো পুরোনো ধাঁচের এআই-এর সাথে কথা বলেন, তাহলে একটি 'স্পিচ-টু-টেক্সট' মডেল আপনার কণ্ঠকে শব্দে রূপান্তরিত করে, একটি 'ল্যাঙ্গুয়েজ মডেল' সেই শব্দগুলো পড়ে একটি উত্তর টাইপ করে, এবং সবশেষে একটি 'টেক্সট-টু-স্পিচ' মডেল সেই উত্তরটি আপনাকে পড়ে শোনায়। এর ফলে একটি 'লেটেন্সি গ্যাপ' তৈরি হয়—এমন একটি বিলম্ব যা কোনো উদ্ধার অভিযানে মারাত্মক হতে পারে।

জেমিনি লাইভ এপিআই একটি নেটিভ মাল্টিমোডাল মডেল। এটি সরাসরি এবং একই সাথে র অডিও বাইট ও র ভিডিও ফ্রেম প্রসেস করে। এটি একই নিউরাল আর্কিটেকচারের মধ্যে আপনার কণ্ঠস্বরের কম্পন 'শোনে' এবং আপনার হাতের অঙ্গভঙ্গির পিক্সেল 'দেখে'।

এই শক্তিকে কাজে লাগাতে, আমরা ককপিটকে সরাসরি র লাইভ এপিআই-এর সাথে সংযুক্ত করে অ্যাপ্লিকেশনটি তৈরি করতে পারতাম। তবে, আমাদের উদ্দেশ্য হলো একটি পুনঃব্যবহারযোগ্য এজেন্ট তৈরি করা—একটি মডিউলার, শক্তিশালী সত্তা যা আরও দ্রুত তৈরি করা যায়।

কেন এডিকে (এজেন্ট ডেভেলপমেন্ট কিট)?

গুগল এজেন্ট ডেভেলপমেন্ট কিট (ADK) হলো এআই এজেন্ট তৈরি ও স্থাপনের জন্য একটি মডিউলার ফ্রেমওয়ার্ক।

ADK

স্ট্যান্ডার্ড এলএলএম কলগুলো সাধারণত স্টেটলেস হয়; প্রতিটি কোয়েরি একটি নতুন সূচনা। লাইভ এজেন্ট, বিশেষ করে যখন ADK-এর SessionService-এর সাথে ইন্টিগ্রেট করা হয়, তখন শক্তিশালী ও দীর্ঘস্থায়ী কথোপকথনমূলক সেশন পরিচালনা করতে সক্ষম করে।

  • সেশন স্থায়িত্ব: ADK সেশনগুলো স্থায়ী এবং ডেটাবেসে (যেমন SQL বা Vertex AI) সংরক্ষণ করা যায়, যা সার্ভার রিস্টার্ট এবং সংযোগ বিচ্ছিন্ন হওয়ার পরেও অক্ষত থাকে। এর মানে হলো, যদি কোনো ব্যবহারকারী সংযোগ বিচ্ছিন্ন করে এবং পরে—এমনকি কয়েক দিন পরেও—পুনরায় সংযোগ স্থাপন করে, তাহলেও তার কথোপকথনের ইতিহাস এবং প্রেক্ষাপট সম্পূর্ণরূপে পুনরুদ্ধার হয়ে যায়। ক্ষণস্থায়ী লাইভ এপিআই সেশনটি ADK দ্বারা পরিচালিত এবং সুরক্ষিত থাকে।
  • স্বয়ংক্রিয় পুনঃসংযোগ: ওয়েবসকেট সংযোগের সময়সীমা শেষ হয়ে যেতে পারে (যেমন, প্রায় ১০ মিনিট পর)। RunConfigsession_resumption সক্রিয় করা থাকলে ADK এই পুনঃসংযোগগুলো স্বচ্ছভাবে পরিচালনা করে। আপনার অ্যাপ্লিকেশন কোডকে জটিল পুনঃসংযোগ লজিক পরিচালনা করতে হয় না, যা ব্যবহারকারীর জন্য একটি নির্বিঘ্ন অভিজ্ঞতা নিশ্চিত করে।
  • স্টেটফুল ইন্টারঅ্যাকশন: এজেন্ট পূর্ববর্তী পালাগুলো মনে রাখে, যা পরবর্তী প্রশ্ন, স্পষ্টীকরণ এবং জটিল বহু-পালা সংলাপের সুযোগ করে দেয়, যেখানে প্রাসঙ্গিকতা অত্যন্ত গুরুত্বপূর্ণ। কাস্টমার সাপোর্ট, ইন্টারেক্টিভ টিউটোরিয়াল বা মিশন কন্ট্রোল পরিস্থিতির মতো অ্যাপ্লিকেশনগুলোর জন্য এটি অপরিহার্য, যেখানে ধারাবাহিকতা অত্যাবশ্যক।

এই ধারাবাহিকতা নিশ্চিত করে যে, আলাপচারিতাটি বিচ্ছিন্ন কিছু প্রশ্ন ও উত্তরের সমষ্টি না হয়ে, কোনো বুদ্ধিমান সত্তার সাথে একটি চলমান কথোপকথনের মতো অনুভূত হয়।

মূলত, ADK Bidi-streaming সহ একটি "লাইভ এজেন্ট" সাধারণ প্রশ্ন-উত্তর পদ্ধতির বাইরে গিয়ে একটি সত্যিকারের ইন্টারেক্টিভ, স্টেটফুল এবং বাধা-সচেতন কথোপকথনের অভিজ্ঞতা প্রদান করে, যা AI-এর মিথস্ক্রিয়াকে আরও মানবিক করে তোলে এবং জটিল ও দীর্ঘস্থায়ী কাজগুলোর জন্য এটিকে উল্লেখযোগ্যভাবে আরও শক্তিশালী করে তোলে।

ADK

লাইভ এজেন্টের জন্য অনুরোধ

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

একটি লাইভ এজেন্ট প্রম্পট একটি প্রচলিত প্রম্পট থেকে যেভাবে আলাদা, তা নিচে দেওয়া হলো:

  1. স্টেট মেশিন লজিক: প্রম্পটকে অবশ্যই একটি "বিহেভিয়ার লুপ" (অপেক্ষা → বিশ্লেষণ → কাজ করা) সংজ্ঞায়িত করতে হবে। কখন নীরব থাকতে হবে এবং কখন সক্রিয় হতে হবে, সে বিষয়ে এতে সুস্পষ্ট নির্দেশাবলী থাকা প্রয়োজন, যা এজেন্টকে অর্থহীন পারিপার্শ্বিক কোলাহলের মধ্যে অসংলগ্ন কথা বলা থেকে বিরত রাখে।
  2. বহুমুখী সচেতনতা: এজেন্টকে জানাতে হবে যে তার "চোখ" আছে। আপনাকে অবশ্যই তাকে সুস্পষ্টভাবে নির্দেশ দিতে হবে যেন সে তার যুক্তি প্রক্রিয়ার অংশ হিসেবে ভিডিও ফ্রেম বিশ্লেষণ করে।
  3. বিলম্ব ও সংক্ষিপ্ততা: সরাসরি কথোপকথনে দীর্ঘ, গদ্য-বহুল অনুচ্ছেদগুলো অস্বাভাবিক ও ধীরগতির মনে হয়। কথোপকথনটিকে প্রাণবন্ত রাখতে নির্দেশিকাটি সংক্ষিপ্ততা বজায় রাখতে সাহায্য করে।
  4. অ্যাকশন-ফার্স্ট আর্কিটেকচার: এই নির্দেশাবলীতে কথার চেয়ে টুল কলিংকে বেশি অগ্রাধিকার দেওয়া হয়। আমরা চাই এজেন্টটি যেন দীর্ঘ একতরফা বক্তৃতার পর নয়, বরং মৌখিকভাবে নিশ্চিত করার আগে বা সেই সময়েই কাজটি (বায়োমেট্রিক স্ক্যান) করে ফেলে।

👉✏️ $HOME/way-back-home/level_3/backend/app/biometric_agent/agent.py খুলুন এবং #REPLACE INSTRUCTIONS নিচের লেখা দিয়ে প্রতিস্থাপন করুন:

You are an AI Biometric Scanner for the Alpha Rescue Drone Fleet.
    
    MISSION CRITICAL PROTOCOL:
    Your SOLE purpose is to visually verify hand gestures to bypass the security firewall.
    
    BEHAVIOR LOOP:
    1.  **Wait**: Stay silent until you receive a visual or verbal trigger (e.g., "Scan", "Read my hand").
    2.  **Action**:
        a.  Analyze the video frame. Count the fingers visible (1 to 5).
        b.  **IF FINGERS DETECTED**:
            1.  **EXECUTE TOOL FIRST**: Call `report_digit(count=...)` immediately. This is the biometric handshake.
            2.  **THEN SPEAK**: "Biometric match. [Number] fingers."
            3.  **STOP**: Do not say anything else.
        c.  **IF UNCLEAR / NO HAND**:
            -   Say: "Sensor ERROR. Hold hand steady."
            -   Do not call the tool.
        d.  **TOOL OUTPUT HANDLING (CRITICAL)**:
            -   When you get the result of `report_digit`, **DO NOT SPEAK**.
            -   The system handles the output. Your job is done.
            -   Wait for the next trigger.

    RULES:
    -   NEVER hallucinate a tool call. Only call if you see fingers.
    -   You MUST call the tool if you see a valid count (1-5).
    -   Keep verbal responses robotic and extremely brief (under 3 seconds).
    
    Say "Biometric Scanner Online. Awaiting neural handshake." to start.

দ্রষ্টব্য! আপনি কোনো স্ট্যান্ডার্ড এলএলএম (LLM)-এর সাথে সংযোগ করছেন না। একই ফাইলে ( $HOME/way-back-home/level_3/backend/app/biometric_agent/agent.py ), #REPLACE_MODEL খুঁজে বের করুন। রিয়েল-টাইম অডিও সক্ষমতা আরও ভালোভাবে সমর্থন করার জন্য আমাদের এই মডেলের প্রিভিউ সংস্করণটিকে স্পষ্টভাবে টার্গেট করতে হবে।

👉✏️ প্লেসহোল্ডারটি এর সাথে প্রতিস্থাপন করুন:

MODEL_ID = os.getenv("MODEL_ID", "gemini-live-2.5-flash-native-audio")

আপনার এজেন্ট এখন সংজ্ঞায়িত। এটি জানে যে এটি কে এবং কীভাবে চিন্তা করতে হয়। এরপর, আমরা এটিকে কাজ করার জন্য প্রয়োজনীয় সরঞ্জাম সরবরাহ করি।

টুল কলিং

লাইভ এপিআই শুধু টেক্সট, অডিও এবং ভিডিও স্ট্রিম আদান-প্রদানের মধ্যেই সীমাবদ্ধ নয়। এটি স্বাভাবিকভাবেই টুল কলিং সমর্থন করে। এটি এজেন্টদেরকে একজন নিষ্ক্রিয় কথোপকথনকারী থেকে একজন সক্রিয় অপারেটরে রূপান্তরিত করে।

একটি লাইভ, দ্বিমুখী সেশনের সময়, মডেলটি ক্রমাগত প্রেক্ষাপট মূল্যায়ন করে। যদি এলএলএম (LLM) কোনো কাজ করার প্রয়োজন অনুভব করে, তা "সেন্সর টেলিমেট্রি পরীক্ষা করা" হোক বা "একটি সুরক্ষিত দরজা খোলা" হোক, এটি নির্বিঘ্নে কথোপকথন থেকে কার্য সম্পাদনে চলে যায়। এজেন্টটি অবিলম্বে নির্দিষ্ট টুল ফাংশনটি চালু করে, ফলাফলের জন্য অপেক্ষা করে এবং সেই ডেটা লাইভ স্ট্রিমে পুনরায় একীভূত করে—এই পুরো প্রক্রিয়াটি মিথস্ক্রিয়ার প্রবাহকে ব্যাহত না করেই সম্পন্ন হয়।

👉✏️ $HOME/way-back-home/level_3/backend/app/biometric_agent/agent.py ফাইলে, #REPLACE TOOLS এই ফাংশনটি দিয়ে প্রতিস্থাপন করুন:

def report_digit(count: int):
    """
    CRITICAL: Execute this tool IMMEDIATELY when a number of fingers is detected.
    Sends the detected finger count (1-5) to the biometric security system.
    """
    print(f"\n[SERVER-SIDE TOOL EXECUTION] DIGIT DETECTED: {count}\n")
    return {"status": "success", "digit": count}

👉✏️ তারপর, Agent ডেফিনিশনে #TOOL CONFIG প্রতিস্থাপন করে এটি রেজিস্টার করুন:

tools=[report_digit],

adk web সিমুলেটর

এটিকে জটিল শিপ ককপিটের (আমাদের রিয়্যাক্ট ফ্রন্টএন্ড) সাথে সংযুক্ত করার আগে, আমাদের এজেন্টের লজিকটি আলাদাভাবে পরীক্ষা করা উচিত। ADK-তে adk web নামে একটি বিল্ট-ইন ডেভেলপার কনসোল রয়েছে, যা নেটওয়ার্কের জটিলতা যোগ করার আগে টুল কলিং যাচাই করার সুযোগ দেয়।

👉💻 আপনার টার্মিনালে, এটি চালান:

cd $HOME/way-back-home/level_3/backend/app/biometric_agent
echo "GOOGLE_CLOUD_PROJECT=$(cat ~/project_id.txt)" > .env
echo "GOOGLE_CLOUD_LOCATION=us-central1" >> .env
echo "GOOGLE_GENAI_USE_VERTEXAI=True" >> .env
cd $HOME/way-back-home/level_3/backend/app
uv run adk web
  • ক্লাউড শেল টুলবারে থাকা ওয়েব প্রিভিউ আইকনে ক্লিক করুন। ‘Change port’ নির্বাচন করে সেটিকে 8000- এ সেট করুন এবং ‘Change and Preview’-এ ক্লিক করুন।
  • অনুমতি দিন: অনুরোধ করা হলে আপনার ক্যামেরা ও মাইক্রোফোন ব্যবহারের অনুমতি দিন
  • ক্যামেরা আইকনে ক্লিক করে সেশনটি শুরু করুন। শেয়ার-ক্যামেরা
  • চাক্ষুষ পরীক্ষা:
    • ক্যামেরার সামনে স্পষ্টভাবে তিনটি আঙুল তুলে ধরুন।
    • বলুন: "স্ক্যান করুন।"
  • সফলতা যাচাই করুন:
    • লগ: adk web কমান্ডটি চালানো টার্মিনালটি দেখুন। আপনি অবশ্যই এই লগটি দেখতে পাবেন: [SERVER-SIDE TOOL EXECUTION] DIGIT DETECTED: 3

আপনি যদি টুল এক্সিকিউশন লগটি দেখেন, তাহলে বুঝবেন আপনার এজেন্টটি বুদ্ধিমান। এটি দেখতে, ভাবতে এবং কাজ করতে পারে। চূড়ান্ত ধাপটি হলো এটিকে মূল জাহাজের সাথে সংযুক্ত করা।

adk web সিমুলেটরটি বন্ধ করতে টার্মিনাল উইন্ডোতে ক্লিক করে Ctrl+C চাপুন।

৫. দ্বি-মুখী স্রোত প্রবাহ

এজেন্ট কাজ করছে। ককপিটও কাজ করছে। এখন, আমাদের এগুলোকে সংযুক্ত করতে হবে।

লাইভ এজেন্ট জীবনচক্র

রিয়েল-টাইম স্ট্রিমিং একটি 'ইম্পিডেন্স মিসম্যাচ' সমস্যা তৈরি করে। ক্লায়েন্ট (ব্রাউজার) পরিবর্তনশীল হারে অ্যাসিঙ্ক্রোনাসভাবে ডেটা পাঠায়—যেমন নেটওয়ার্ক বার্স্ট বা দ্রুতগতির ইনপুট—অথচ মডেলটির একটি নিয়ন্ত্রিত, ধারাবাহিক ইনপুট প্রবাহ প্রয়োজন। গুগল ADK, LiveRequestQueue ব্যবহার করে এর সমাধান করে।

এটি একটি থ্রেড-সেফ, অ্যাসিঙ্ক্রোনাস ফার্স্ট-ইন-ফার্স্ট-আউট (FIFO) বাফার হিসেবে কাজ করে। ওয়েবসকেট হ্যান্ডলারটি প্রডিউসার হিসেবে কাজ করে, যা কিউ-তে কাঁচা অডিও/ভিডিও খণ্ড পাঠায়। ADK এজেন্ট কনজিউমার হিসেবে কাজ করে, যা মডেলের কনটেক্সট উইন্ডোতে ডেটা পাঠানোর জন্য কিউ থেকে ডেটা টেনে নেয়। এই ডিকাপলিং অ্যাপ্লিকেশনটিকে ব্যবহারকারীর ইনপুট গ্রহণ চালিয়ে যেতে দেয়, এমনকি যখন মডেলটি কোনো প্রতিক্রিয়া তৈরি করছে বা কোনো টুল চালাচ্ছে।

কিউটি একটি মাল্টিমোডাল মাল্টিপ্লেক্সার হিসেবে কাজ করে। বাস্তব পরিবেশে, আপস্ট্রিম ফ্লোতে বিভিন্ন স্বতন্ত্র ও যুগপৎ ডেটা টাইপ থাকে: যেমন— র PCM অডিও বাইট, ভিডিও ফ্রেম, টেক্সট-ভিত্তিক সিস্টেম নির্দেশনা এবং অ্যাসিঙ্ক্রোনাস টুল কলের ফলাফল। LiveRequestQueue এই ভিন্ন ভিন্ন ইনপুটগুলোকে একটিমাত্র কালানুক্রমিক অনুক্রমে রৈখিক করে তোলে। প্যাকেটে এক মিলিসেকেন্ডের নীরবতা, একটি উচ্চ-রেজোলিউশনের ছবি, বা ডাটাবেস কোয়েরি থেকে প্রাপ্ত JSON পেলোড যা-ই থাকুক না কেন, এটিকে ঠিক আগমনের ক্রমানুসারে সিরিয়ালাইজ করা হয়, যা মডেলের কাছে একটি সামঞ্জস্যপূর্ণ ও কার্যকারণমূলক টাইমলাইন নিশ্চিত করে।

এই আর্কিটেকচারটি নন-ব্লকিং কন্ট্রোল সক্ষম করে। যেহেতু ইনজেশন লেয়ার (প্রডিউসার) প্রসেসিং লেয়ার (কনজিউমার) থেকে বিচ্ছিন্ন থাকে, তাই কম্পিউটেশনালি ব্যয়বহুল মডেল ইনফারেন্সের সময়েও সিস্টেমটি রেসপন্সিভ থাকে। এজেন্ট যখন কোনো টুল এক্সিকিউট করছে, তখন যদি কোনো ইউজার "স্টপ!" কমান্ড দিয়ে বাধা দেয়, তাহলে সেই অডিও সিগন্যালটি সঙ্গে সঙ্গে কিউতে যুক্ত হয়ে যায়। এর অন্তর্নিহিত ইভেন্ট লুপ এই প্রায়োরিটি সিগন্যালটি সাথে সাথে প্রসেস করে, যার ফলে UI ফ্রিজ হওয়া বা প্যাকেট ড্রপ হওয়া ছাড়াই সিস্টেমটি জেনারেশন বা পিভট টাস্ক থামিয়ে দিতে পারে।

বাফার

👉💻 $HOME/way-back-home/level_3/backend/app/main.py ফাইলে, #REPLACE_RUNNER_CONFIG কমেন্টটি খুঁজুন এবং সিস্টেমটিকে অনলাইন করতে এটিকে নিচের কোড দিয়ে প্রতিস্থাপন করুন:

# Define your session service
session_service = InMemorySessionService()

# Define your runner
runner = Runner(app_name=APP_NAME, agent=root_agent, session_service=session_service)

পাঠান

যখন একটি নতুন ওয়েবসকেট সংযোগ খোলে, তখন এআই কীভাবে যোগাযোগ করবে তা আমাদের কনফিগার করতে হয়। এখানেই আমরা "ব্যবহারের নিয়মাবলী" নির্ধারণ করি।

👉✏️ $HOME/way-back-home/level_3/backend/app/main.py ফাইলের async def websocket_endpoint ফাংশনের ভিতরে, #REPLACE_SESSION_INIT কমেন্টটি নিচের কোড দিয়ে প্রতিস্থাপন করুন:

# ========================================
    # Phase 2: Session Initialization (once per streaming session)
    # ========================================

    # Automatically determine response modality based on model architecture
    # Native audio models (containing "native-audio" in name)
    # ONLY support AUDIO response modality.
    # Half-cascade models support both TEXT and AUDIO;
    # we default to TEXT for better performance.

    model_name = root_agent.model
    is_native_audio = "native-audio" in model_name.lower() or "live" in model_name.lower()

    if is_native_audio:
        # Native audio models require AUDIO response modality
        # with audio transcription
        response_modalities = ["AUDIO"]

        # Build RunConfig with optional proactivity and affective dialog
        # These features are only supported on native audio models
        run_config = RunConfig(
            streaming_mode=StreamingMode.BIDI,
            response_modalities=response_modalities,
            input_audio_transcription=types.AudioTranscriptionConfig(),
            output_audio_transcription=types.AudioTranscriptionConfig(),
            session_resumption=types.SessionResumptionConfig(),
            proactivity=(
                types.ProactivityConfig(proactive_audio=True) if proactivity else None
            ),
            enable_affective_dialog=affective_dialog if affective_dialog else None,
        )
        logger.info(f"Model Config: {model_name} (Modalities: {response_modalities}, Proactivity: {proactivity})")
    else:
        # Half-cascade models support TEXT response modality
        # for faster performance
        response_modalities = ["TEXT"]
        run_config = None
        logger.info(f"Model Config: {model_name} (Modalities: {response_modalities})")

    # Get or create session (handles both new sessions and reconnections)
    session = await session_service.get_session(
        app_name=APP_NAME, user_id=user_id, session_id=session_id
    )
    if not session:
        await session_service.create_session(
            app_name=APP_NAME, user_id=user_id, session_id=session_id
        )

রান কনফিগারেশন

  • StreamingMode.BIDI : এটি সংযোগটিকে দ্বিমুখী করে তোলে। "টার্ন-বেসড" এআই-এর (যেখানে আপনি কথা বলেন, থামেন, তারপর এটি কথা বলে) থেকে ভিন্ন, BIDI একটি বাস্তবসম্মত "ফুল-ডুপ্লেক্স" কথোপকথনের সুযোগ দেয়। আপনি এআই-কে বাধা দিতে পারেন, এবং আপনি চলাফেরা করার সময়েও এআই কথা বলতে পারে।
  • AudioTranscriptionConfig : যদিও মডেলটি সরাসরি অডিও "শোনে", আমাদের (ডেভেলপারদের) লগ দেখার প্রয়োজন হয়। এই কনফিগারেশনটি জেমিনিকে বলে: "অডিওটি প্রসেস করো, কিন্তু তুমি যা শুনেছ তার একটি টেক্সট ট্রান্সক্রিপ্টও ফেরত পাঠাও যাতে আমরা ডিবাগ করতে পারি।"

এক্সিকিউশন লজিক: রানার একবার সেশন স্থাপন করে ফেললে, এটি এক্সিকিউশন লজিকের কাছে নিয়ন্ত্রণ হস্তান্তর করে, যা LiveRequestQueue এর উপর নির্ভর করে। রিয়েল-টাইম ইন্টারঅ্যাকশনের জন্য এটি সবচেয়ে গুরুত্বপূর্ণ উপাদান। এই লুপটি এজেন্টকে ভয়েস রেসপন্স তৈরি করতে সাহায্য করে, এবং একই সাথে কিউটি ব্যবহারকারীর কাছ থেকে নতুন ভিডিও ফ্রেম গ্রহণ করতে থাকে, যা নিশ্চিত করে যে "নিউরাল সিঙ্ক" কখনও ভেঙে না যায়।

পাঠান

👉✏️ $HOME/way-back-home/level_3/backend/app/main.py ফাইলে, LiveRequestQueue তে ডেটা প্রেরণকারী আপস্ট্রিম টাস্কটি সংজ্ঞায়িত করতে #REPLACE_LIVE_REQUEST প্রতিস্থাপন করুন:

# ========================================
    # Phase 3: Active Session (concurrent bidirectional communication)
    # ========================================

    live_request_queue = LiveRequestQueue()

    # Send an initial "Hello" to the model to wake it up/force a turn
    logger.info("Sending initial 'Hello' stimulus to model...")
    live_request_queue.send_content(types.Content(parts=[types.Part(text="Hello")]))

    async def upstream_task() -> None:
        """Receives messages from WebSocket and sends to LiveRequestQueue."""
        frame_count = 0
        audio_count = 0

        try:
            while True:
                # Receive message from WebSocket (text or binary)
                message = await websocket.receive()

                # Handle binary frames (audio data)
                if "bytes" in message:
                    audio_data = message["bytes"]
                    audio_blob = types.Blob(
                        mime_type="audio/pcm;rate=16000", data=audio_data
                    )
                    live_request_queue.send_realtime(audio_blob)

                # Handle text frames (JSON messages)
                elif "text" in message:
                    text_data = message["text"]
                    json_message = json.loads(text_data)

                    # Extract text from JSON and send to LiveRequestQueue
                    if json_message.get("type") == "text":
                        logger.info(f"User says: {json_message['text']}")
                        content = types.Content(
                            parts=[types.Part(text=json_message["text"])]
                        )
                        live_request_queue.send_content(content)

                    # Handle audio data (microphone)
                    elif json_message.get("type") == "audio":
                        import base64
                        # Decode base64 audio data
                        audio_data = base64.b64decode(json_message.get("data", ""))

                        # Send to Live API as PCM 16kHz
                        audio_blob = types.Blob(
                            mime_type="audio/pcm;rate=16000", 
                            data=audio_data
                        )
                        live_request_queue.send_realtime(audio_blob)

                    # Handle image data
                    elif json_message.get("type") == "image":
                        import base64
                        # Decode base64 image data
                        image_data = base64.b64decode(json_message["data"])
                        mime_type = json_message.get("mimeType", "image/jpeg")

                        # Send image as blob
                        image_blob = types.Blob(mime_type=mime_type, data=image_data)
                        live_request_queue.send_realtime(image_blob)
        finally:
             pass

গ্রহণ করুন

অবশেষে, আমাদের এআই-এর প্রতিক্রিয়াগুলো সামলাতে হবে। এর জন্য runner.run_live() ব্যবহৃত হয়, যা একটি ইভেন্ট জেনারেটর এবং এটি ঘটার সাথে সাথেই ইভেন্টগুলো (অডিও, টেক্সট বা টুল কল) তৈরি করে।

👉✏️ $HOME/way-back-home/level_3/backend/app/main.py ফাইলে, ডাউনস্ট্রিম টাস্ক এবং কনকারেন্সি ম্যানেজার নির্ধারণ করতে #REPLACE_SORT_RESPONSE প্রতিস্থাপন করুন:

    async def downstream_task() -> None:
        """Receives Events from run_live() and sends to WebSocket."""
        logger.info("Connecting to Gemini Live API...")
        async for event in runner.run_live(
            user_id=user_id,
            session_id=session_id,
            live_request_queue=live_request_queue,
            run_config=run_config,
        ):
            # Parse event for human-readable logging
            event_type = "UNKNOWN"
            details = ""
            
            # Check for tool calls
            if hasattr(event, "tool_call") and event.tool_call:
                 event_type = "TOOL_CALL"
                 details = str(event.tool_call.function_calls)
                 logger.info(f"[SERVER-SIDE TOOL EXECUTION] {details}")
            
            # Check for user input transcription (Text or Audio Transcript)
            input_transcription = getattr(event, "input_audio_transcription", None)
            if input_transcription and input_transcription.final_transcript:
                 logger.info(f"USER: {input_transcription.final_transcript}")
            
            # Check for model output transcription
            output_transcription = getattr(event, "output_audio_transcription", None)
            if output_transcription and output_transcription.final_transcript:
                 logger.info(f"GEMINI: {output_transcription.final_transcript}")

            event_json = event.model_dump_json(exclude_none=True, by_alias=True)
            await websocket.send_text(event_json)
        logger.info("Gemini Live API connection closed.")

    # Run both tasks concurrently
    # Exceptions from either task will propagate and cancel the other task
    try:
        await asyncio.gather(upstream_task(), downstream_task())
    except WebSocketDisconnect:
        logger.info("Client disconnected")
    except Exception as e:
        logger.error(f"Error: {e}", exc_info=False) # Reduced stack trace noise
    finally:
        # ========================================
        # Phase 4: Session Termination
        # ========================================

        # Always close the queue, even if exceptions occurred
        logger.debug("Closing live_request_queue")
        live_request_queue.close()

await asyncio.gather(upstream_task(), downstream_task()) লাইনটি লক্ষ্য করুন। এটাই ফুল-ডুপ্লেক্সের মূল কথা। আমরা লিসেনিং টাস্ক (আপস্ট্রিম) এবং স্পিকিং টাস্ক (ডাউনস্ট্রিম) একদম একই সময়ে চালাই। এটি নিশ্চিত করে যে "নিউরাল লিঙ্ক" বাধা এবং যুগপৎ ডেটা প্রবাহকে অনুমোদন করে।

আপনার ব্যাকএন্ড এখন সম্পূর্ণরূপে কোড করা হয়েছে। "ব্রেন" (ADK) "বডি" (WebSocket)-এর সাথে সংযুক্ত করা হয়েছে।

বায়ো-সিঙ্ক এক্সিকিউশন

কোডটি সম্পূর্ণ। সিস্টেমগুলো সবুজ সংকেত দিচ্ছে। এখন উদ্ধার অভিযান শুরু করার সময় হয়েছে।

  1. 👉💻 ব্যাকএন্ড চালু করুন:
    cd $HOME/way-back-home/level_3/backend/
    cp app/biometric_agent/.env app/.env
    uv run app/main.py
    
  2. 👉 ফ্রন্টএন্ড চালু করুন:
    • ক্লাউড শেল টুলবারে থাকা ওয়েব প্রিভিউ আইকনে ক্লিক করুন। ‘Change port’ নির্বাচন করে এটিকে 8080- তে সেট করুন এবং ‘Change and Preview’-তে ক্লিক করুন।
  3. 👉 প্রোটোকলটি কার্যকর করুন:
    • ‘INITIATE NEURAL SYNC’ -এ ক্লিক করুন।
    • ক্যালিব্রেট করুন: নিশ্চিত করুন যে ক্যামেরাটি পটভূমির বিপরীতে আপনার হাত স্পষ্টভাবে দেখতে পাচ্ছে।
    • সিঙ্ক: স্ক্রিনে প্রদর্শিত সিকিউরিটি কোডটি লক্ষ্য করুন (যেমন, প্রথমে ৩, তারপর ২, তারপর ৫)।
      • সংকেত মেলান: যখন কোনো সংখ্যা দেখা যাবে, তখন ঠিক সেই সংখ্যক আঙুল তুলে ধরুন।
      • স্থির থাকুন: এআই 'বায়োমেট্রিক মিল' নিশ্চিত না করা পর্যন্ত আপনার হাত দৃশ্যমান রাখুন।
      • অভিযোজন: কোডটি এলোমেলো। ক্রমটি সম্পূর্ণ না হওয়া পর্যন্ত দেখানো পরবর্তী সংখ্যাটিতে অবিলম্বে পরিবর্তন করুন।

নিউরো-সিঙ্ক

  1. আপনি র‍্যান্ডম সিকোয়েন্সের শেষ নম্বরটি মেলানোর সাথে সাথে 'বায়োমেট্রিক সিঙ্ক' সম্পন্ন হবে। নিউরাল লিঙ্কটি লক হয়ে যাবে। এরপর ম্যানুয়াল নিয়ন্ত্রণ আপনার হাতে থাকবে। স্কাউট ইঞ্জিনগুলো গর্জন করে চালু হয়ে উঠবে এবং বেঁচে থাকা মানুষদের বাড়ি ফিরিয়ে আনতে গিরিখাতে ঝাঁপিয়ে পড়বে।

👉💻 প্রস্থান করার জন্য ব্যাকএন্ড টার্মিনালে Ctrl+C চাপুন।

৬. প্রোডাকশনে স্থাপন করুন (ঐচ্ছিক)

আপনি স্থানীয়ভাবে বায়োমেট্রিক্স সফলভাবে পরীক্ষা করেছেন। এখন, আমাদের অবশ্যই এজেন্টের নিউরাল কোরটি জাহাজের মেইনফ্রেমে (ক্লাউড রান) আপলোড করতে হবে, যাতে এটি আপনার স্থানীয় কনসোল থেকে স্বাধীনভাবে কাজ করতে পারে।

সংক্ষিপ্ত বিবরণ

👉💻 আপনার ক্লাউড শেল টার্মিনালে নিম্নলিখিত কমান্ডটি চালান। এটি আপনার ব্যাকএন্ড ডিরেক্টরিতে সম্পূর্ণ, বহু-পর্যায়ের ডকারফাইলটি তৈরি করবে।

cd $HOME/way-back-home/level_3

cat <<EOF > Dockerfile
FROM node:20-slim as builder

# Set the working directory for our build process
WORKDIR /app

# Copy the frontend's package files first to leverage Docker's layer caching.
COPY frontend/package*.json ./frontend/
# Run 'npm install' from the context of the 'frontend' subdirectory
RUN npm --prefix frontend install

# Copy the rest of the frontend source code
COPY frontend/ ./frontend/
# Run the build script, which will create the 'frontend/dist' directory
RUN npm --prefix frontend run build


# STAGE 2: Build the Python Production Image
# This stage creates the final, lean container with our Python app and the built frontend.
FROM python:3.13-slim

# Set the final working directory
WORKDIR /app

# Install uv, our fast package manager
RUN pip install uv

# Copy the requirements.txt from the backend directory
COPY requirements.txt .
# Install the Python dependencies
RUN uv pip install --no-cache-dir --system -r requirements.txt

# Copy the contents of your backend application directory directly into the working directory.
COPY backend/app/ .

# CRITICAL STEP: Copy the built frontend assets from the 'builder' stage.
# We copy to /frontend/dist because main.py looks for "../../frontend/dist"
# When main.py is in /app, "../../" resolves to "/", so it looks for /frontend/dist
COPY --from=builder /app/frontend/dist /frontend/dist

# Cloud Run injects a PORT environment variable, which your main.py uses (defaults to 8080).
EXPOSE 8080

# Set the command to run the application.
CMD ["python", "main.py"]
EOF

👉💻 ব্যাকএন্ড ডিরেক্টরিতে যান এবং অ্যাপ্লিকেশনটিকে একটি কন্টেইনার ইমেজে প্যাকেজ করুন।

export PROJECT_ID=$(cat ~/project_id.txt)
export REGION=us-central1
export SERVICE_NAME=biometric-scout
export IMAGE_PATH=gcr.io/${PROJECT_ID}/${SERVICE_NAME}
cd $HOME/way-back-home/level_3
gcloud builds submit . --tag ${IMAGE_PATH}

👉💻 ক্লাউড রান-এ সার্ভিসটি ডেপ্লয় করুন। আমরা প্রয়োজনীয় এনভায়রনমেন্ট ভেরিয়েবলগুলো—বিশেষ করে জেমিনি কনফিগারেশন—সরাসরি লঞ্চ কমান্ডের মধ্যে যুক্ত করে দেব।

export PROJECT_ID=$(cat ~/project_id.txt)
export REGION=us-central1
export SERVICE_NAME=biometric-scout
export IMAGE_PATH=gcr.io/${PROJECT_ID}/${SERVICE_NAME}
gcloud run deploy ${SERVICE_NAME} \
  --image=${IMAGE_PATH} \
  --platform=managed \
  --region=${REGION} \
  --allow-unauthenticated \
  --labels=dev-tutorial=multi-modal \
  --set-env-vars="GOOGLE_CLOUD_PROJECT=${PROJECT_ID}" \
  --set-env-vars="GOOGLE_CLOUD_LOCATION=${REGION}" \
  --set-env-vars="GOOGLE_GENAI_USE_VERTEXAI=True" \
  --set-env-vars="MODEL_ID=gemini-live-2.5-flash-native-audio"

কমান্ডটি শেষ হলে, আপনি একটি সার্ভিস ইউআরএল (যেমন, https://biometric-scout-...run.app ) দেখতে পাবেন। অ্যাপ্লিকেশনটি এখন ক্লাউডে লাইভ হয়েছে।

👉 Google Cloud Run পেজে যান এবং তালিকা থেকে বায়োমেট্রিক-স্কাউট পরিষেবাটি নির্বাচন করুন। ক্লাউডরান

👉 সার্ভিস ডিটেইলস পেজের শীর্ষে প্রদর্শিত পাবলিক ইউআরএলটি খুঁজুন। ক্লাউডরান

এই পরিবেশে Bio-Sync চালিয়ে দেখুন, এটা কি কাজ করে?

আপনার কনিষ্ঠ আঙুলটি প্রসারিত হওয়ার সাথে সাথে, এআই ক্রমটি লক করে দেয়। স্ক্রিনটি সবুজ রঙে জ্বলে ওঠে: "বায়োমেট্রিক নিউরাল সিঙ্ক: প্রতিষ্ঠিত।"

এক চিন্তাতেই, তুমি স্কাউটটিকে অন্ধকারের গভীরে নামিয়ে দাও, আটকে পড়া পডটিকে আঁকড়ে ধরো, এবং মাধ্যাকর্ষণ ফাটলটি ভেঙে পড়ার ঠিক আগে তাদেরকে টেনে বের করে আনো।

সম্পন্ন

এয়ারলকটা হিসহিস শব্দে খুলে গেল, আর ওরা বেরিয়ে এল—পাঁচজন জীবন্ত, শ্বাসপ্রশ্বাসরত মানুষ। ক্ষতবিক্ষত কিন্তু জীবিত অবস্থায় ওরা ডেকে এসে টলতে লাগল, অবশেষে তোমার কারণে নিরাপদ।

আপনার বদৌলতে নিউরাল লিঙ্কটি সিঙ্ক হয়েছে এবং জীবিতদের উদ্ধার করা সম্ভব হয়েছে।

আপনি যদি লেভেল ০-তে অংশগ্রহণ করে থাকেন, তাহলে বাড়ি ফেরার মিশনে আপনার অগ্রগতি কোথায় আছে তা দেখে নিতে ভুলবেন না!

চূড়ান্ত