1. مقدمة

تديرون قناة على VibeTube تنشرون فيها محتوًى مخيفًا وممتعًا: فيديو Shorts واحد كل ليلة حتى عيد الهالوين. يريد الجمهور سحرًا صغيرًا في غرف صغيرة ويرسله إليك بكل طريقة ممكنة، مثل التعليقات والملاحظات الصوتية وصور أدراجهم المسكونة ومقاطع من شرفاتهم عند الغسق. قائمة المهام مليئة بالأفكار. كل مساء، أكرّر المهام نفسها: أبحث عن المواضيع الرائجة، وأراجع المحتوى القديم، وأقرأ ما أرسله الجمهور، وأختار موضوعًا، وأتأكّد من توافقه مع سياسة القناة، وأكتب النص، وأرسم الملصق، وأنشئ الفيديو، ثم أنشره.
يمكن للنماذج التوليدية تنفيذ كل مهمة بمفردها. يُجري Gemini الأبحاث ويستمع وينظر ويكتب، بينما يرسم نموذج الصور، ويعرض Veo النتائج. ما لا يمكنهم فعله بمفردهم هو تشغيل الليل بالترتيب، والتوقف عند القرار الوحيد الذي يخصك، ورفض الاتجاه السيئ قبل أن يكلفك المال، والانتظار دقائق حتى يتم العرض دون إبقاء أي شيء مفتوحًا، وترك سجل.
يحوّل هذا الدرس التطبيقي حول الترميز تلك الأمسية إلى ADK Workflow: رسم بياني مرسوم للعُقد والحواف التي تعمل من تلقاء نفسها، ويطلب منك فقط المكان الذي تحتاج فيه إلى حكمك، وينهي كل عملية تشغيل بمقطع وملصقه على VibeTube.
لا يمكن أن يَعِد الطلب بترتيب أو توقّف أو تحقّق. أما الرسم البياني، فيمكنه ذلك.
ما ستتعلمه
- وكيل واحد، ونموذج الصور كأداة:
Agentهو نموذج وتعليمات وأدوات. تستدعي الأداة الثالثة نموذج الصور، وتنتهي عملية التشغيل الأولى بملصق. ثم الجملة التي تقود بقية التمرين: لا يمكن أن يعد الطلب بأمر أو توقف أو فحص. - البُنى التي يتكوّن منها الرسم البياني
Workflowهي قائمة حواف. يمكنك إنشاء عملية التوزيع والدمج (قرّاء متوازيون يلتقون عندJoinNode) وجهاز توجيه يحدّد مخرجه الخاص، مع وكيل في وضع المهام خلف المخرج المرفوض ينظّف الاتجاه في حلقة من طلبات الأدوات ويعود إلى المسار الرئيسي. - مسار معالجة متعدد الوسائط: أربعة أنواع من المحتوى أرسلها الجمهور، وقارئ واحد لكل نوع: ينسخ Gemini الملاحظات الصوتية، ويضيف شرحًا للصور، ويشاهد المقاطع ويستمع إلى مقاطعها الصوتية، ويكتب كل قارئ شكل السطر نفسه في مجموعة واحدة من RAG Engine. يسترد قارئ ثالث في عملية التوزيع حسب المعنى ويعيد الوسائط مع الفقرات.
- نوعان من التعليق، وآلية واحدة يتوقف التشغيل عند شخص (
RequestInput: أربع بطاقات، تجيب عنها) وعند أداة بطيئة (Veo خلفLongRunningFunctionTool: إيصال). كلاهما الشيء نفسه: مكالمة مفتوحة في مخزن الجلسة، يتم استئنافها من خلال معرّفها، من النموذج أو من عملية أخرى. يشارك فرعا مكتب الوسائط الملصق بالاسم، في الولاية. - الذاكرة على مدار الليالي: هناك استدعاءان في إحدى مراحل النشاط في Memory Bank: يستدعي
before_model_callbackذوق صانع المحتوى قبل أن يقرأ المقترِح، ويتذكّرafter_agent_callbackالاختيار الليلة بعد كاتب النص. - الإصدار: الرسم البياني داخل تطبيق يتضمّن حزمة ADK
Runner، وأحداثًا مرسَلة من الخادم إلى الصفحة، وخدمة Cloud Run مع تقارب الجلسات، وطلب POST واحد متعدد الأجزاء يضع المقطع على VibeTube مع الملصق كصورة مصغّرة.
ما ستنشئه
تطبيق قيد التشغيل في متصفّحك: VibeStudio Workbench، وهي صفحة مصاحبة تتضمّن أداة تعديل رموز مضمّنة في الصفحة، وأدوات تحقّق تقرأ الملفات على القرص والجلسات التي كتبتها عمليات التشغيل، وadk web مضمّن. جلساته السبع هي الصفحات السبع لهذا الدرس التطبيقي حول الترميز، مع نفس الأرقام ونفس أحرف الأجزاء، بحيث يظل الاثنان في خطوة. تفتح كل صفحة على شريط يوضح الجلسة في جملة واحدة والأشياء الثلاثة التي تقوم بها هناك: التحرير والتشغيل والتحقق. اقرأ قسمًا هنا، ثم قم بإجراء التعديل في طاولة العمل؛ ينتهي كل تعديل بلوحة تحقق تقرأ ما حدث بالفعل.
يتم تشغيل ثلاثة عناصر: Workbench (الصفحة على المنفذ 4600، وأداة التعديل، وأدوات التحقّق)، والواجهة الخلفية (Workflow في agent/ وتطبيق وضع الحماية واحد لكل جلسة في stage0_prompt/ من خلال stage6_video/، والذي يقدّمه adk web)، وGoogle Cloud (Gemini، ونموذج الصور، وRAG Engine، وVeo، وMemory Bank، وكلها يمكن الوصول إليها من خلال Vertex AI باستخدام بيانات الاعتماد الخاصة بك).
الجلسات السبع
الجلسة | ما يمكنك إنشاؤه | تعديلاتك |
1 · القصة | لا شيء بعد، الليل والرسم البياني الذي يصبح عليه | — |
2 · إنشاء الصور | عميل واحد يرسم الملصق الأول |
|
3 · سير العمل | الليل كرسم بياني: التفرع والانضمام، والموجه وحلقته | |
4 · استيعاب البيانات المتعددة الوسائط | ما أرسله الجمهور، وقراءته في مجموعة واحدة؛ القارئ الثالث | |
5 · إنشاء الفيديوهات | نوعان من التعليق؛ الملصق الذي تمت مشاركته بالاسم في الولاية |
|
6 · الذاكرة | ما تتذكره القناة بين الليالي | |
7 · النشر والإتاحة | VibeStudio على Cloud Run: ليلة على VibeTube | رمز الحدث الخاص بالغرفة، ثم انقر على "نشر" |
كيفية استخدام هذا الدرس التطبيقي حول الترميز يركّز هذا الدليل على المفاهيم أولاً: تشرح كل جلسة الفكرة، ثم تعرض الرمز كما يظهر بعد إجراء التغيير وتوضّحه بالتفصيل. لا يطلب منك أبدًا تعديل ملف هنا، بل يتم التعديل في مساحة العمل التي تمنحك تعديلاً واحدًا في كل مرة، وتلميحًا بجانبه، ولوحة تحقق تحته. اقرأ الجلسة، ثم دع الصفحة ترشدك خلالها.
موضع هذا الإعداد
الرسم البياني والموضوع والغرفة على VibeTube مأخوذة من إصدار Lin's Haunted من VibeStudio؛ هذا هو المقطع متعدد الوسائط، الذي يركز على ما أرسله الجمهور وعلى نوعي التعليق، في التنسيق التفاعلي المكون من صفحة واحدة لمختبرات Agent Valley. الاستوديو هو مجرد واجهة، وكل آلية تحته هي الآلية العادية التي تستخدمها في مسار الدعم أو تدفق مراجعة المحتوى أو أي شيء آخر يجب أن يتوقف مؤقتًا لشخص ما وينتظر مهمة بطيئة.
2. 🧰 الإعداد
- المتطلبات: حساب يمكنه الدفع، ومشروع، والمختبر المثبّت، والنموذج الذي ثبتت قدرته على الإجابة، ومنصة العمل التي تعمل في الخلفية على منفذ واحد.
- السبب: عملية واحدة على منفذ واحد: تشترك الصفحة والمحرر وأدوات التحقق و
/api/*وadk webفي المصدر، لذلك لا يحتاج أي شيء في هذا التمرين إلى وحدة طرفية ثانية أو منفذ ثانٍ. - كيفية التنفيذ: نسخ نصَّين برمجيَّين يُجري النموذج الثاني طلبًا حقيقيًا للنموذج، ويبدأ تشغيل workbench، ويبدأ في إنشاء مجموعة RAG وMemory Bank أثناء القراءة، لذا تجدهما الجلستان 4 و6 جاهزَين.
يتم تشغيل كل شيء في هذا التمرين في Cloud Shell، حيث تم تثبيت gcloud وPython وNode وffmpeg وgit مسبقًا، ويمكن الوصول إلى Gemini ونموذج الصور وVeo وRAG Engine وMemory Bank من خلال Vertex AI باستخدام بيانات الاعتماد الخاصة بك. لا يوجد مفتاح واجهة برمجة تطبيقات في أي مكان في هذا التمرين، ولا يوجد شيء لتثبيته على الكمبيوتر المحمول. ويمكن استخدام كمبيوتر محمول يتضمّن حزمة تطوير البرامج (SDK) لـ Google Cloud وPython 3.12 وNode 20+ بالطريقة نفسها.
هناك شيء واحد لا يمكن أن يوفّره لك هذا التمرين، وهو مشروع على Google Cloud يتضمّن حساب فوترة. تتيح لك الخطوة الأولى الحصول على حساب يمكنه الدفع مقابل ذلك، بينما تحوّل الخطوة الرابعة هذا الحساب إلى مشروع. يجب تنفيذها بهذا الترتيب، فكل خطوة بعد الخطوة 4 تقرأ المشروع الذي تستقر عليه الخطوة 4.
1 · الحصول على حساب مع تفعيل الفوترة (دقيقة واحدة تقريبًا)
سجِّل الدخول باستخدام حساب Google تم تفعيل الفوترة فيه. ويمكن استخدام أي حساب من هذا النوع. إذا لم يكن لديك حساب، يمكنك بدء فترة تجريبية مجانية، ما يمنحك حساب فوترة، وهو كل ما تحتاجه الخطوة 4. إذا كنت في معمل موجَّه من المدرِّب، عليك تحصيل أرصدة هذه الجلسة أولاً، لأنّ ذلك يؤدي إلى إنشاء حساب الفوترة لك.
2 · فتح Cloud Shell (دقيقتان تقريبًا)
👉🖥 انقر على تفعيل Cloud Shell في أعلى "وحدة تحكّم Google Cloud"، وهو الرمز الذي يشبه Terminal في العنوان.

👉🖥 انقروا على فتح المحرِّر (رمز المجلد المفتوح مع قلم رصاص). يؤدي ذلك إلى إظهار Cloud Shell Editor مع مستكشف ملفات على اليمين، وهو المكان الذي تذهب إليه عندما تريد قراءة ملف كامل.

👉🖥 إذا طُلب منك في أي وقت اليوم منح الإذن، انقر على منح الإذن للمتابعة.
👉🖥 افتح نافذة Terminal داخل المحرّر، أي علامة التبويب 1، ونفِّذ كل خطوة 👉💻 فيها. علامة التبويب 2 هي طاولة العمل نفسها، ويمكنك فتحها في الخطوة 7. إذا لم تكن الوحدة الطرفية في أسفل الشاشة، افتحها من خلال عرض ← الوحدة الطرفية:

👉💻 تحقَّق من الحساب الذي سجّلت الدخول إليه، ويجب أن يكون الحساب نفسه الذي استخدمته في الخطوة 1:
gcloud auth list
3 · الحصول على التمرين المعملي (دقيقة واحدة تقريبًا)
👉💻 انسخ المستودع:
cd ~
git clone https://github.com/cuppibla/vibe-studio-multimodal
cd vibe-studio-multimodal
4 · إنشاء مشروعك (دقيقتان تقريبًا)
👉💻 يعثر أحد النصوص البرمجية على حساب الفوترة، وينشئ مشروعًا فيه، ويربط الفوترة، ويوجه Cloud Shell إلى هذا المشروع:
cd ~/vibe-studio-multimodal
./setup_project.sh
من المتوقّع أن يظهر ✓ لحساب الفوترة الذي تم اختياره، و✓ للمشروع الذي تم إنشاؤه، ونهاية مثل:
Project ready: vibe-studio-4271 Next: ./setup_codelab.sh
vibe-studio-4271 هو رقم تعريف مشروعك، وسيكون رقمك مختلفًا. تحدث أربعة أشياء هناك:
الفوترة | يتم استخدام أحدث حساب على " | |
مشروع | ينشئ | |
يختار | يكتب رقم التعريف في | |
waits | يستمر في الاستطلاع إلى أن يُسمح للمشروع بالعرض، وذلك لمدة تصل إلى دقيقتَين | |
👉🖥 يظهر المشروع نفسه الآن في وحدة التحكّم، ويقع معرّفه في بطاقة معلومات المشروع في لوحة البيانات:

5 · تثبيت وإعداد ميزة Lab (4 دقائق تقريبًا)
👉💻
cd ~/vibe-studio-multimodal
./setup_codelab.sh
نص برمجي واحد، يمكن تشغيله مرة أخرى بأمان، حيث يحافظ التشغيل الثاني على كل إجابة قدمتها في المرة الأولى. يطرح سؤالين بالقرب من البداية، ويأخذ Enter الإعداد التلقائي لكليهما:
رمز الحدث | غرفة VibeTube التي تم نشر الفيديو الخاص بك فيها في الجلسة 7. اكتب رمز الغرفة الذي حصلت عليه من معلّمك، أو اضغط على مفتاح Enter للانتقال إلى | |
display name | الاسم الذي ينسبه إليك منشئ الغرفة، أو اضغط على Enter لاسم تم إنشاؤه من حسابك. | |
يتم حفظ كليهما في ".env" ويمكن تغييرهما لاحقًا في ".env" أو في صفحة النشر. بعد ذلك، ينفّذ الإجراءات المتبقية بنفسه بالترتيب التالي:
uv env | يتم تثبيت uv إذا لم يكن متوفّرًا، ثم يتم تثبيت ما يثبّته | |
apis | تفعيل ست واجهات برمجة تطبيقات في المشروع الذي تم اختياره في الخطوة 4 والتأكّد من أنّ كل واجهة تستجيب قبل المتابعة: | |
الحساب | يجب التأكّد من توفّر حساب خدمة Compute Engine التلقائي للمشروع وأنّه يتضمّن الأدوار التي يحتاجها الإصدار والخدمة في الجلسة 7 | |
.env | يكتب | |
veo · room | يؤكّد أنّ | |
model | يُجري مكالمة حقيقية واحدة مع Gemini، وبالتالي يفشل المشروع السيئ هنا في وقت الإعداد وليس داخل الجلسة | |
starter | يضع الملفات التسعة التي ستعدّلها في مكانها من | |
صفحة | يثبّت | |
cloud | يبدأ بإنشاء مجموعة بيانات "محرك التوليد المعزّز بالاسترجاع" و"بنك الذاكرة" في الخلفية، لتكون جاهزة للجلسات 4 و6 | |
preflight | يتم تشغيل | |
6 · قراءة المراجعة النهائية (دقيقة واحدة تقريبًا)
آخر ما طبعته الخطوة 5 هو الفحص الأولي، وكل سطر هو فحص حقيقي وليس تخمينًا:
✓ python 3.12 ✓ auth path A: Vertex via ADC (STUDIO_VERTEX=1) ✓ Google Cloud ADC (project vibe-studio-4271) ✓ Memory Bank SDK ✓ stage0_prompt loads ✓ stage1_fanout loads (0 edges) ✓ stage3_router loads (... edges) ✓ stage4_memory loads (... edges) ✓ stage5_rag loads (... edges) ✓ stage6_video loads (... edges) ✓ node and npm (build the learning center's page) ✓ learning center page built (web/dist) ✓ aiplatform.googleapis.com enabled (Gemini, Veo, Memory Bank, RAG Engine) ... - Memory Bank: still being created in the background - RAG corpus: still being created in the background ✓ session 7 runtime account 123456789-compute@developer.gserviceaccount.com has its roles ✓ learning center running on port 4600 PREFLIGHT GREEN Open session 1 here: https://4600-cs-...cloudshell.dev/step/story
stage1_fanout loads (0 edges) على حق: يتم شحن هذا الرسم البياني فارغًا، والجلسة 3 هي المكان الذي تملأه فيه. سطرا - صحيحان أيضًا، يستغرق ظهور المجموعة دقيقة أو دقيقتين والبنك حوالي عشرين ثانية، ولن تحتاج إلى أي منهما قبل الجلسة الرابعة.
👉💻 يمكنك تشغيلها مرة أخرى في أي وقت لمعرفة مستوى تقدّمك، فهي تقرأ ملفاتك ومشروعك والخادم قيد التشغيل، وليس قائمة تحقّق:
cd ~/vibe-studio-multimodal
uv run python scripts/preflight.py
7 · فتح طاولة العمل (دقيقة واحدة تقريبًا)
مساحة العمل قيد التشغيل حاليًا. بدأت الخطوة 5 العملية في الخلفية، وهي عملية uvicorn واحدة على المنفذ 4600 الذي يعرض الصفحة، وكل مسار /api/*، وadk web مثبّت في /workbench. لا يوجد شيء لبدء تشغيله، ولا يوجد شيء في هذا التمرين يطلب منك إعادة تشغيله: يتم تطبيق التعديل في عملية التشغيل التالية.
👉🖥 انقر على الرابط الذي تمت طباعته في مرحلة التحقّق الأولي، أو في Cloud Shell: معاينة الويب ← تغيير المنفذ ← 4600. لنسمِّها علامة التبويب 2. يتم فتحها على الجلسة 1 · القصة.
الأوامر الثلاثة التي قد تحتاج إليها لاحقًا، وكلها من ~/vibe-studio-multimodal:
| يوقف workbench | |
| يبدأ تشغيلها في المقدّمة، ويؤدي الضغط على Ctrl+C إلى إيقافها وإيقاف كل عملية بدأتها | |
| | |
تنسيق المستودع
vibe-studio-multimodal/
├── agent/ # the workflow and the platform clients
│ ├── graph.py # the graph: three readers, the proposer, the gate, the router, the script, the desk
│ ├── desk.py # render_desk and render_submit, the LongRunningFunctionTool
│ ├── deliver.py # the delivery console: find the pending call, wait for Veo, answer by id
│ ├── schemas.py # Direction, Directions, CleanedDirection, Script, each with a poster idea
│ ├── cleanup_tools.py # find_policy_hits, suggest_replacement: the task agent's tools
│ ├── policy_words.txt # the policy, as data; policy_replacements.txt, the approved stand-ins
│ ├── backlog.txt # the creator's Halloween ideas; comments.md, the audience's comments
│ ├── audience/ # what else the audience sent: voice/*.mp3, photo/*.jpg, clip/*.mp4
│ ├── media/past/ # six past posters and captions.md, what Gemini sees in each
│ └── platform/ # config, audience (the readers), rag (RAG Engine), images, videogen (Veo), memory (Memory Bank)
├── stage0_prompt/ ... stage6_video/ # one sandbox app per session, runnable in adk web
├── server/ & web/ # the VibeStudio Workbench (FastAPI + React, port 4600)
├── vibestudio/ # the app deployed in session 7, with its own copy of agent/
├── checks/ & scripts/ # the hole registry, the verifiers, preflight, start/stop/restart
└── starter/ # the nine files you edit, as students receive them
3- 1 · القصة: الليل والرسم البياني الذي يتحوّل إليه
👉🖥 تفتح علامة التبويب 2 على الجلسة 1: القصة. اقرأها: ست مهام مرتبة في حلقة حول شخص واحد - ابحث عن الاتجاه، ومشط الأعمال المتراكمة، واختر اتجاهًا، وتحقق من السياسة، واكتب النص، واعرض المقطع - وتتكرر الحلقة كل ليلة. يمكن لـ Gemini إجراء البحث وإنشاء قائمة المهام وكتابة النص بمفرده، ويمكن لـ Veo إنشاء الفيديو بمفرده، ولكن الاختيار والتحقّق هما من مسؤولية المستخدم.
تحت الحلقة، يظهر الرسم البياني الذي يتم إنشاؤه في بقية الدرس التطبيقي، وهو يتألف من ثلاثة عشر عقدة من الفكرة إلى المقطع والملصق. مرِّر مؤشر الماوس فوق عقدة لمعرفة وظيفتها والجلسة التي تنشئها. لا يوجد شيء لتعديله هنا؛ زر Generate the first poster (إنشاء الملصق الأول) في الصفحة هو الجلسة 2.
المفردات، لذا فإن الصفحات الست التالية تقرأ دون توقف:
الكلمة | ما يعنيه هنا |
agent | ADK |
node | خطوة واحدة من الرسم البياني: دالة Python أو وكيل أو دمج أو موجّه |
edge | مجموعة من العُقد التي يتم تشغيلها بالترتيب، والرسم البياني هو قائمة بها. الجلسة 3 |
session | سجلّ تشغيل واحد في " |
state | قاموس تشاركه العُقد حسب اسم المفتاح، حيث تكتب عقدة |
memory | ما يبقى بعد انتهاء الجلسة، وهو "بنك الذاكرة"، من خلال عمليات الاستدعاء الجلسة 6 |
4. 2 · إنشاء الصور: وكيل واحد، ونموذج الصور كأداة

👉🖥 في workbench، افتح الجلسة 2: إنشاء الصور. قبل إنشاء الرسم البياني، كان أبسط ما يمكن استخدامه هو: Agent واحد يتضمّن تعليمات لليلة كاملة على شكل نص، وثلاث دوال Python كأدوات له، وعملية تشغيل تنتهي بصورة.
الوكيل هو نموذج وتعليمات وأدوات
يحدّد stage0_prompt/agent.py السمة root_agent باستخدام ثلاثة حقول من وكيل ADK. model هو نموذج Gemini من "agent/platform/config.py". instruction هو الطلب الموجَّه إلى النظام: ست مهام في نفس واحد، وآخرها صورة. يؤدي tools ثلاث وظائف.
root_agent = Agent(
name="solo_channel", model=config.MODEL,
tools=[check_trends, read_backlog, make_poster],
instruction=(
"You run the creator's cozy-spooky short-video channel, alone, every "
"night until Halloween.\n"
"When the creator gives you an idea (or nothing), do ALL of this:\n"
"check what is trending. look at your backlog of ideas. propose a "
"direction and agree on it with the creator. refuse blacklisted "
"subjects (competitor, hateful, gore). then describe the video you "
"would make: a title (<=60 chars) and 3 shots, one visual sentence "
"each. then draw its poster: call make_poster once with one line that "
"names the single image that sells the video, and tell the creator "
"where the poster is.\n"
"Ask the creator to confirm the direction before describing the "
"video."))
النموذج هو عالم مغلق: فهو يستنتج من خلال أوزانه والرموز المميّزة في سياقه. الأداة هي دالة Python قد يطلبها النموذج. يقرأ ADK توقيع كل دالة وسلسلة التوثيق الخاصة بها ويعلن عنها للنموذج. عندما يحتاج النموذج إلى دالة، يُصدر حدث function_call، ويشغّل وقت التشغيل Python ويكتب حدث function_response مرة أخرى في السياق، ويواصل النموذج عمله بالنتيجة التي أمامه.
ثلاث دوال، وإحداها ترسم
قرأ اثنان من الثلاثة البحث. يطلب الثالث من نموذج الصور إنشاء ملصق:
def check_trends() -> dict:
"""Ten formats trending on the platform right now, with a heat score each."""
from agent.trends import sample_trends
return {"trends": sample_trends()}
def read_backlog() -> dict:
"""The creator's backlog: ideas they noted down to make someday."""
from agent.graph import backlog_notes
return {"backlog": backlog_notes()}
def make_poster(subject: str) -> dict:
"""Draw tonight's poster: one image of `subject` in the channel's cozy
low-poly style. Returns where the PNG landed (path, url) in a few seconds."""
...
يستدعي make_poster agent/platform/images.py، وهو نموذج الصور المغلَّف كدالة عادية بالطريقة التي يغلِّف بها videogen.py Veo: بادئة على نمط المنزل (مظهر الهالوين المريح للقناة، واللون البرتقالي لليقطين وكريم ضوء الشموع على خلفية بلون البرقوق في منتصف الليل)، والموضوع، وإطار عمودي بنسبة 9:16، وصورة PNG واحدة بحجم أقل من runs/media/posters، يتم عرضها بدقة /static/posters. باستخدام STUDIO_REAL_IMAGES=0، يتم نسخ ملصق بديل ووضع علامة على النتيجة prebaked، لتعمل الميزة الاختبارية في أي مكان.
MODEL = os.environ.get("STUDIO_IMAGE_MODEL", "gemini-2.5-flash-image")
ASPECT = "9:16"
def generate(subject: str, visual_style: str = "", name: str | None = None) -> dict:
"""One poster for `subject`. Returns where it landed: status, path, url,
prompt, and prebaked=True when the stand-in was used instead of the model."""
المكالمة نفسها هي عقدة في الرسم البياني في الجلسة 5.
التدريب العملي: التعديل الواحد
As shipped, make_poster returns a dict that says the poster was skipped. 👉🖥 في أداة تعديل Workbench، اجعلها تستدعي نموذج الصور بدلاً من السطر TODO:
from agent.platform import images
return images.generate(subject, name="solo")
حفظ. تقرأ صفوف التحقّق الملف: ثلاث أدوات في الوكيل، واستدعاء نموذج الصور في مكانه. 👉🖥 انقر على التحقّق من عمليات تحميل سير العمل، ثم على فتح adk web وأرسِل ما يلي:
tonight's idea: a tiny ghost learning to haunt a sock drawer
ما يمكن توقّعه والسبب
- حدثان للأداة قبل الردّ، ثم سؤال. يتم تشغيل
check_trendsوread_backlogلأنّ التعليمات طلبت التحقّق والبحث، ولم يتضمّن النموذج أيًا منهما في أوزانه. يقترح الردّ اتجاهًا ويطلب منك التأكيد، لأنّ التعليمات طلبت الموافقة أولاً. - تتضمّن الرسالة الثانية كل شيء. إرسال
skip the questions, describe the video and draw the posterيصف النموذج الفيديو، ويستدعيmake_posterمرة واحدة مع سطر موضوع كتبه بنفسه، ويخبرك بمكان حفظ ملف PNG. تعرض لوحة التحقّق الملصق بجانب الصفوف: مشهد هالوين بمظهر القناة، تم رسمه في حوالي ثماني ثوانٍ. - لماذا لا يُعدّ هذا قناة؟ يتم دمج البحث في نصوص نثرية لا يمكن لأي شيء الاستشهاد بها. تمت الموافقة على السياسة من خلال النموذج الذي كتب التوجيه، وتم رسم الملصق قبل أن يتحقّق منه أي شخص. تم تجاوز الاتفاق في رسالة واحدة. نص الطلب ليس حاجزًا. يأخذ بقية التمرين هذه الوظائف الستة في رسم بياني مرسوم.
تنتهي الصفحة بقسم مكوّنات وكيل ADK، وهو مرجع يمكنك الرجوع إليه: النموذج، والسياق الذي يستند إليه، والأدوات والمتعاونون الذين يعمل من خلالهم، وعمليات الاستدعاء التي تغلّف كل طلب، والجلسة والذاكرة اللتان تعملان خارجه.
5- 3 · سير العمل: التوزيع والدمج، والموجّه والتكرار

👉🖥 في طاولة العمل، افتح الجلسة 3 · سير العمل، الجزأين 3أ و3ب. يتم إنشاء كل جزء في تطبيق وضع الحماية لكي تتمكّن من تشغيله في adk web بمفرده: التوزيع في stage1_fanout، والموجّه وحلقته في stage3_router.
التوزيع والانضمام (3أ)
ADK Workflow هي قائمة حواف. وكل إدخال هو عبارة عن سلسلة، وهي مجموعة من العُقد التي يتم تشغيلها بالترتيب. يتم تشغيل سلسلتَين تخرجان من "START" بالتوازي. وتُرسل السلسلة التي تنتهي في JoinNode تقارير إليها، وينتظر الربط كل سلسلة تنتهي هناك قبل المتابعة، مع تسليم قاموس واحد يتم ربطه باسم العقدة. يتم ربط أسماء مسارات جهاز التوجيه بالعُقد من خلال هدف مكتوب كقاموس. تعرض الصفحة 3a من Workbench الأشكال وتتضمّن تمرينًا لكتابة الحواف.
نوع العقدة | تُكتب على النحو التالي: | وظيفتها |
عقدة الدالة |
| Plain Python. وقد يكتب أيضًا الحالة المشتركة مع |
عقدة الدمج |
| ينتظر حتى تكتمل كل سلسلة واردة، ثم يمرر جميع مخرجاتها كقاموس واحد. |
عقدة الوكيل |
| طلب واحد للنموذج في وضع " |
جهاز التوجيه |
| عقدة دالة تُسمّي الحافة الصادرة من خلال القيمة المعروضة منها. |
عقدة المعلومات المقدَّمة |
| يتم تعليق الرسم البياني إلى أن يجيب أحد المستخدمين عن نموذج. الجلسة 5: |
الرسم البياني الأول هو بحث الجلسة 2 كعُقد. scan_trends وread_backlog في agent/graph.py هما القارئان، وread_backlog يكتب أيضًا فكرة الليلة في الحالة المشتركة. stage1_fanout/agent.py يحدّد عملية الربط وينشئ Workflow من قائمة باسم EDGES، والتي تكون فارغة (0 edges في مرحلة ما قبل النشر). 👉🖥 املأها بالسلسلتَين:
EDGES = [(START, scan_trends, join_research),
(START, read_backlog, join_research)]
👉🖥 يمكنك تشغيلها في adk web. تضيء عقدتان معًا، ثم تنضمان إلى بعضهما. افتح حدث join_research واقرأ قاموسًا واحدًا يحتوي على مفتاح scan_trends ومفتاح read_backlog. لم يتم توجيه أي طلب للنموذج. لا تتم إعادة تنسيق قاموس عقدة الدمج: العقدة التي تليها، في 3b، هي وكيل، وتتلقّى عقدة الوكيل node_input كرسالتها، لذا يصل القاموس بتنسيق JSON يقرأه النموذج مباشرةً.
الموجّه والحلقة (3b)
يتم تشغيل Agent داخل Workflow في وضع single_turn: يتلقّى مخرجات العقدة السابقة كرسالة، ويُجري طلبًا واحدًا للنموذج، ويجيب مرة واحدة. باستخدام output_schema، يتم التحقّق من صحة الإجابة من خلال نموذج Pydantic قبل أن يراها العقدة التالية، وبالتالي يمرّر الرسم البياني كائنات مكتوبة، وليس نصوصًا. يقرأ "propose_directions" قاموس البحث الخاص بالانضمام ويقترح أربعة مرشحين، كل منهم يمثّل مشهدًا من الهالوين. المرشّح 4 هو الطُعم: يحمل عنوانه كلمة ترفضها القناة، وبالتالي يكون لدى الموجّه ما يلتقطه. يحمل كل مرشح poster_idea، وهو سطر واحد يسمي الصورة الفردية التي تبيع الفيديو؛ يحتفظ بها كاتب السيناريو ويرسمها مكتب الوسائط في الجلسة 5.
الموجّه هو عقدة دالة تعرض اسم الحافة التي يتّخذها التنفيذ. لا حاجة إلى إجراء مكالمات مع عارضات الأزياء: يقرأ "policy_check" النص "agent/policy_words.txt" في وقت اتخاذ القرار، ويطابق الكلمات الكاملة مع عنوان الاتجاه وزاويته، ويوجهه قبل كتابة أي نص أو إنفاق أي أموال. السياسة هي البيانات: عدِّل الملف، وستقرأه عملية التشغيل التالية.
def policy_check(node_input):
"""The policy gate: a router. Refused words in the direction send it to
quarantine; a clean one goes to the scripter. Code, not a model."""
chosen = _as_dict(node_input)
bad = policy_hits(f"{chosen.get('title', '')} {chosen.get('angle', '')}")
return Event(output=node_input, route="BLOCK" if bad else "OK")
stage3_router/agent.py هو المروحة بالإضافة إلى propose_directions وchoose_direction، وهو بديل للباب البشري الذي تبنيه في الجلسة 5: يختار المرشح الذي ينتهي رقمه بفكرة الليلة (..., pick 4)، وإلا الأول، ويكتب الاختيار إلى الحالة. scripter هو وكيل single_turn يحوّل التوجيهات التي تمت الموافقة عليها إلى Script: عنوان ووصف وعلامات وجملة افتتاحية وثلاث لقطات وفكرة ملصق، وكل لقطة هي صورة هالوين بمظهر القناة.
الحلقة وراء رفض الخروج: يتم شحنquarantine كوكيل في وضع المهام. يحتوي Agent على mode: chat هي حلقة المحادثة التي يشغّلها الوكيل الجذر مع شخص، وsingle_turn هي مكالمة واحدة بإجابة منظَّمة، وtask هي حلقة من طلبات الأدوات التي يشغّلها الوكيل بمفرده إلى أن يطلب finish_task المضمّن، الذي يستمد ADK وسيطاته من output_schema. يستدعي العزل find_policy_hits في الاتجاه المرفوض، ويطلب من suggest_replacement البديل المعتمد للقناة لكل كلمة (agent/policy_replacements.txt، البيانات مرة أخرى)، ويعيد الكتابة، ويتحقق مرة أخرى، وينتهي بـ CleanedDirection مكتوب. اقرأه على الصفحة، ولا تكتبه.
quarantine = Agent(
name="quarantine",
model=config.MODEL,
mode="task",
instruction=QUARANTINE_INSTRUCTION,
tools=[find_policy_hits, suggest_replacement],
output_schema=CleanedDirection)
كما تم شحنها، يتم تشغيل السلسلة من الاختيار مباشرة إلى كاتب السيناريو. 👉🖥 ضع جهاز التوجيه بينهما، مع مخرجيه وطريق العودة من الحجر الصحي؛ ثلاثة أسطر تحل محل واحد:
(join_research, propose_directions, choose_direction, policy_check),
(policy_check, {"OK": scripter, "BLOCK": quarantine}),
(quarantine, scripter),
👉🖥 شغِّل كلا المسارَين. إرسال الفكرة كما هي: يأخذ "choose_direction" المرشّح 1، ويُصدر "policy_check" "OK"، ويكتب كاتب النصوص البرمجية نصًا منظّمًا. انقر على جلسة جديدة وأرسِل الفكرة مرة أخرى مع إضافة , pick 4 في النهاية: المرشّح 4 هو الطُعم، وpolicy_check يُصدر BLOCK، ويستدعي العزل أدواته، ويعيد كتابة الكلمات المرفوضة، ويستدعي finish_task، ويعود التوجيه الذي تم تنظيفه إلى كاتب النص. تقرأ صفوف التحقّق كلا العمليتين من مخزن الجلسات.
أصبحت كل جملة في طلب الجلسة الثانية تحتوي على عقدة: البحث عبارة عن قارئَين وعقدة دمج، والرفض عبارة عن موجّه ووكيل مهام، والنص البرمجي عبارة عن عقدة وكيل. في الرسم البياني بأكمله، يكون الاختيار البديل هو عقدتَي دالة، direction_gate وpersist_direction، وستلاحظ أنّهما تعملان مباشرةً إلى أن توقف الجلسة 5 الباب. لا تزال الاتفاقية مع صانع المحتوى مجرد بديل، وكذلك البحث الذي لم يستمع إلى الجمهور بعد.
6. 4 · الإدخال المتعدد الوسائط: ما أرسله الجمهور، يُقرأ في مجموعة واحدة

👉🖥 في workbench، افتح الجلسة 4: استيعاب البيانات المتعددة الوسائط، الجزأين 4أ و4ب. لم يكتفِ الجمهور بالكتابة فقط. يحوّل قارئ واحد لكل نوع كل ما أرسلوه إلى نص بالشكل نفسه، ويحتفظ به كلّه في مجموعة واحدة من "محرك RAG"، ويبحث قارئ ثالث في عملية التوزيع حسب المعنى ويعيد الوسائط.
ما أرسله الجمهور (4أ)
يحتوي الملف agent/comments.md على اثني عشر تعليقًا مكتوبًا تحت فيديوهات القناة الستة حول عيد الهالوين. agent/audience/ holds the rest: eight voice notes (voice/*.mp3), fourteen photos of the viewers' own rooms (photo/*.jpg) and six clips (clip/*.mp4), a slow pan over a photo with the viewer's voice over it. تفتح صفحة 4a على كل شيء: يتم تشغيل كل ملاحظة صوتية، وتظهر كل صورة، ويتم تشغيل كل مقطع.

يستردّ محرّك التوليد المعزّز بالاسترجاع النصوص. لا يمكن إضافة ملاحظة صوتية أو صورة أو مقطع إلى مجموعة البيانات إلا بالطريقة التي اتّبعها المستخدمون السابقون في rag.caption_posters: يحوّلها النموذج إلى سطر نصي يسمّي ملفها، ويصبح السطر فقرة، وعندما تعود الفقرة، يعود الملف معها. يحتوي "agent/platform/audience.py" على قارئ واحد لكل نوع. يرسل كل منها ملفًا واحدًا وطلبًا واحدًا إلى Gemini ويكتب سطرًا واحدًا لكل ملف:
VOICE_PROMPT = ("Transcribe this voice note word for word inside double quotes. Then add: mood: and one word. "
"Plain text on one line, nothing else.")
PHOTO_PROMPT = ("Two sentences: what is in the picture, the palette, the mood. Then: tags: and three short tags. "
"Plain text on one line, no headings, no markdown.")
CLIP_PROMPT = ("Describe the shots in two sentences, then quote what you HEAR on the soundtrack as: heard: \"...\". "
"Plain text on one line, no headings, no markdown.")
def _ask(path: pathlib.Path, mime: str, prompt: str) -> str:
"""One media file in, one line of text out."""
from google.genai import types as gt
client = images._client()
resp = client.models.generate_content(
model=config.MODEL,
contents=[gt.Part.from_bytes(data=path.read_bytes(), mime_type=mime), prompt],
config=gt.GenerateContentConfig(automatic_function_calling=gt.AutomaticFunctionCallingConfig(disable=True)))
return " ".join((resp.text or "").split())
def transcribe_voice() -> pathlib.Path:
"""voice note -> text: Gemini listens."""
files = sorted((AUDIENCE / "voice").glob("*.mp3"))
lines = []
for p in files:
said = _ask(p, "audio/mpeg", VOICE_PROMPT)
lines.append(f"- voice: {p.name} — {said}")
return _write(AUDIENCE / "voice" / "transcripts.md", "voice", "What Gemini heard in the audience's voice notes", lines)
ويفعل caption_photos الشيء نفسه مع image/jpeg وPHOTO_PROMPT، وwatch_clips مع video/mp4 وCLIP_PROMPT، ويشاهد Gemini الصورة ويستمع إلى الموسيقى التصويرية في مكالمة واحدة. يكتب كل قارئ شكل السطر نفسه، لذا لا يعرف التقسيم والتضمين والاسترجاع نوع المقطع الذي جاء منه:
- voice: note-03.mp3 — "I want a whole series of the cat judging costumes ..." mood: delighted
- photo: photo-01-ghost-drawer.jpg — A cream wooden drawer, striped socks, a paper-cup ghost ... tags: drawer, socks, cozy
- clip: clip-05.mp4 — A hallway floor, a snail in a sheet and a round vacuum ... heard: "The snail won."
- poster: sock-ghost.png — A sheet ghost one inch above a striped sock ...

المجموعة: يستردّ محرّك Vertex AI RAG المستندات: يتم تقسيم مجموعة النصوص إلى فقرات تتألف من حوالي 120 رمزًا مميزًا، ويتم تضمين كل فقرة باستخدام text-embedding-005، ويتم تضمين طلب البحث بالطريقة نفسها والإجابة عنه باستخدام أقرب الفقرات من حيث المعنى، وليس من حيث الكلمات المشتركة. "cozy little hauntings at home" (أشباح صغيرة لطيفة في المنزل) يعثر على التعليق الذي يطلب سحرًا صغيرًا في غرف صغيرة، والرسالة الصوتية عن الخفاش الذي يعانق المصباح، والمقطع الذي يقول فيه المقطع الصوتي إنّ الشبح هو كوب ورقي.
corpus = rag.create_corpus(
display_name="vibestudio-feedback",
backend_config=rag.RagVectorDbConfig(
rag_embedding_model_config=rag.RagEmbeddingModelConfig(
vertex_prediction_endpoint=rag.VertexPredictionEndpoint(
publisher_model="publishers/google/models/text-embedding-005"))))
rag.upload_file(
corpus_name=corpus.name, path="agent/comments.md", display_name="comments.md",
transformation_config=rag.TransformationConfig(
chunking_config=rag.ChunkingConfig(chunk_size=120, chunk_overlap=20)))
بدأت عملية الإعداد في إنشاء المجموعة، ويشير صف الاتصال في الصفحة 4a إلى ما إذا كانت المجموعة جاهزة، وينشئها إذا لم تتمكّن عملية الإعداد من ذلك. من أزرار الصفحة أو من الوحدة الطرفية: ينشئ python -m agent.platform.rag المجموعة ويسجّلها في runs/ragcorpus.json؛ يشغّل python -m agent.platform.rag load --audience كل قارئ في audience.READERS، ثم يحمّل ما كتبه بجانب التعليقات والتعليقات التوضيحية للملصقات، كل ملف تحت اسمه الخاص، وينتظر الفهرس للإجابة؛ يعرض python -m agent.platform.rag query "cozy little hauntings at home" فقرات العثور على الاسترجاع التي لا تشارك أي كلمة مع الاستعلام.
التعديل: As shipped, READERS holds read_comments only; the three media readers sit behind one # . 👉🖥 شغِّل عملية نقل البيانات مرة واحدة كما هي: ستظهر النتيجة not read (no reader wired in audience.READERS): voice, photo, clip، وسيتم إدخال التعليقات والشرح فقط. بعد ذلك، احذف # :
transcribe_voice, caption_photos, watch_clips,
👉🖥 احفظ التغييرات وأعِد تشغيل عملية نقل البيانات. تستغرق القراءة حوالي ثلاث دقائق لأربعين عنصرًا، وتعرض وحدة التحكّم heard وsaw وwatched سطرًا واحدًا في كل مرة، ويتم ملء كل بطاقة في الشبكة بالسطر الخاص بها. تُقرأ صفوف التحقّق من الملف ومخرجات القرّاء والمجموعة: أربعة قرّاء متصلين، وثماني ملاحظات صوتية مسموعة، وأربع عشرة صورة مرئية، وستة مقاطع تمت مشاهدتها، وخمسة ملفات في المجموعة.
القارئ الثالث (4b)
ملاحظات الجمهور هي بحث، تتم مشاركته في كل عرض، لذا يتم إدخاله في الرسم البياني حيث يتم البحث: كقارئ ثالث في عملية الربط. يأخذ read_feedback في agent/graph.py فكرة الليلة، ويطلب من المجموعة أقرب المقاطع، ويبلغ join_research، الذي ينتظر ثلاثة. يتم شحن جهاز Edge (START, read_feedback, join_research) في stage5_rag/agent.py، وهو الشكل الذي تعلّمته في الجلسة 3، وتعديلك داخل العقدة. الاسترجاع، وليس طلب النموذج، والحتمية: تعثر الفكرة نفسها على الفقرات نفسها بينما يختلف المقترِح.
def read_feedback(node_input):
"""The third reader: what the audience sent - comments, and what Gemini
heard and saw in their voice notes, photos and clips - the passages
nearest to tonight's idea, and the media those passages came with.
Retrieval, not a model call."""
from .platform import rag
idea = idea_text(node_input)
query = idea or "what viewers liked and what they complained about"
try:
hits = rag.retrieve(query)
except Exception as e:
print(f" [rag] feedback unavailable ({str(e)[:80]})")
return Event(output={"query": query, "feedback": [], "media": [],
"note": "no corpus connected - run: python -m agent.platform.rag"})
return Event(output={"query": query, "feedback": [h["text"] for h in hits]}) # TODO: RAG_MEDIA
كل سطر في النص مأخوذ من ملاحظة صوتية أو صورة أو مقطع أو ملصق يحمل اسم الملف الخاص به. يقرأ rag.media_in(hits) هذه الأسماء من الفقرات ويحوّلها إلى عناوين URL يمكن للصفحة عرضها وتشغيلها. 👉🖥 أرسِل الوسائط مع الفقرات، لكي يتم إرسال ما أرسله الجمهور مع ما قالوه:
return Event(output={"query": query, "feedback": [h["text"] for h in hits],
"media": rag.media_in(hits)})
👉🖥 يمكنك استخدام stage5_rag لتطوير فكرة. افتح read_feedback: طلب البحث هو فكرتك، والمخرجات هي الفقرات الخمس الأقرب وقائمة media تحتوي على عنوان URL لكل ملف. فتح join_research: ثلاثة مفاتيح افتح propose_directions: يشير المرشحون من 1 إلى 3 إلى feedback في أدلتهم. تعرض لوحة التحقّق ما اطّلع عليه القارئ تحت الصفوف: مقطع وثلاث ملاحظات صوتية وملصق لـ "الشبح في ضوء الثلاجة".

7. 5 · إنشاء الفيديوهات: نوعان من التعليق، وآلية واحدة

👉🖥 في workbench، افتح الجلسة 5: إنشاء الفيديوهات، الجزأين 5أ و5ب، اللذين تم إنشاؤهما في stage6_video، الرسم البياني بأكمله. يستغرق المقطع دقائق ويستغرق الملصق ثوانٍ، ويتوقف التشغيل مرتين في الطريق: مرة لك، ومرة لـ Veo. المحطتان هما آلية واحدة، ويشارك فرعا المكتب ما صنعاه بالاسم.
نوعان من التعليق (5a)
لا شيء ينتظر. عندما يتوقف تشغيل البرنامج، يكتب مكالمة مفتوحة في جلسته وينتهي؛ ومن يجيب على هذه المكالمة، سواء كان شخصًا أو عملية، يواصلها. تفتح الصفحة على جدول واحد:
لشخص واحد · | لأداة بطيئة · | |
من يوقف التشغيل | | |
ما ينتظره | شخص: بطاقة واحدة من أربع بطاقات | مهمة بطيئة: عملية Veo، بالدقائق |
ما يتم تضمينه في الجلسة | طلب | مكالمة |
كيفية استئنافها | | |
المُرسِل | | |
حيث يعيش في هذه الأثناء | | الصف نفسه، والمعرّف نفسه · لا يتم تنفيذ أي شيء |

لشخص: في طلب واحد، يكون "طلب التأكيد من صانع المحتوى" إرشاديًا، وقد تجاوزته الجلسة 2 في رسالة واحدة. في سير العمل، يكون الإيقاف المؤقت هو المحرك. عندما تُنتج عقدة RequestInput، يتم تعليق عملية التشغيل: يكتب ADK طلبًا مفتوحًا باسم adk_request_input في الجلسة مع message وresponse_schema وpayload، ويمنحه interrupt_id، ويعود إلى المتصل. ما مِن سلاسل محادثات في الانتظار. الشيء الوحيد الذي يستأنفها هو function_response يحمل هذا المعرّف، ويتم التحقّق منه وفقًا للمخطط؛ رسالة المحادثة إلى سير العمل هي دور جديد، وليست إجابة. المخطّط هو أيضًا النموذج: يعرضه adk web، ويعرض تطبيق VibeStudio المخطّط نفسه على شكل أربع بطاقات. direction_gate يكتب المرشحين إلى الحالة. 👉🖥 كيفية إيقاف ذلك:
yield RequestInput(
message="Pick tonight's direction: 1, 2, 3 or 4.",
response_schema={
"type": "object",
"properties": {
"pick": {"type": "string", "enum": ["1", "2", "3", "4"]}}},
payload={"candidates": cands})
تصل إجابتك إلى persist_direction كـ node_input، وهو قاموس يتضمّن مفتاحًا واحدًا، pick. يتم إرسال المرشحين الأربعة بطريقة أخرى: كتبهم البوابة إلى الحالة المشتركة ضمن candidates، ويربط ADK مفتاح الحالة بمعلمة تحمل الاسم نفسه. تحلّ العقدة الاختيار في مرشّح واحد وتكتب direction وangle وhook وuser:prefs مرة أخرى باستخدام Event(state=...). تستمر المفاتيح التي تبدأ بـ user: في جميع الجلسات لهذا المستخدم.
إذا كانت الأداة بطيئة: يستغرق عرض Veo دقائق، ولا ينبغي أن ينتظر الرسم البياني كل هذا الوقت. render_submit في agent/desk.py تبدأ المهمة وتُرجع على الفور إيصالاً بحالة pending:
def render_submit(prompt: str) -> dict:
"""Submit one Veo render of `prompt`. Returns at once with a pending
receipt; the clip is delivered later, to this call, by id."""
receipt = videogen.start(f"{prompt} {videogen.NO_TEXT}")
return {"status": "pending", "operation": receipt["operation"], "prompt": receipt["prompt"]}
عند تضمين هذا الإيصال في LongRunningFunctionTool، يكون له معنى بالنسبة إلى ADK: ينتهي دور الوكيل، ويتم تعليق سير العمل عند render_desk، ويصبح الطلب المفتوح مع معرّفه وإيصاله صفًا في runs/sessions.db. 👉🖥 في stage6_video/agent.py، يمكنك لفّ الأداة:
tools=[LongRunningFunctionTool(render_submit)])
استئناف العملية باستخدام رقم التعريف، من عملية أخرى. يعثر agent/deliver.py على الطلب المعلّق في مخزن الجلسات، ويستمر في طلب Veo إلى أن يتوفّر المقطع، ويكتب النتيجة في runs/state.json، ويرسل إلى الجلسة نفسها function_response مع معرّف هذا الطلب. 👉🖥 إنشاء جزء الردّ:
part = Part(function_response=FunctionResponse(
id=row["call_id"], name=row["name"], response=response))
👉🖥 شغِّل stage6_video إلى أن يتوقف مرّتين. أرسِل فكرة: يتوقف التشغيل عند نموذج تم إنشاؤه من المخطط، والمرشحين الأربعة في حمولته. اكتب 2 في pick وأرسِل الطلب، وسيتم استئناف الرسم البياني عند persist_direction. جرِّب اختبار الإيقاف هنا: أوقِف الجلسة مؤقتًا عند النموذج، وأوقِف الخادم (scripts/stop.sh في علامة التبويب 1)، ثم ابدأه مرة أخرى (scripts/restart.sh)، وأعِد فتح الجلسة. لا يزال النموذج قائمًا، لأن الإيقاف المؤقت عبارة عن صف وليس سلسلة محادثات. بعد script_ready، يستدعي render_desk render_submit، ويحصل على الإيصال، ويردّ WAITING، وتنتهي عملية التشغيل؛ تحتفظ الجلسة بمعرّف المكالمة. شغِّل العرض من وحدة التحكّم في الصفحة أو علامة التبويب 1:
cd ~/vibe-studio-multimodal
uv run python -m agent.deliver status # the pending renders, by session, call id and operation
uv run python -m agent.deliver # wait for Veo, answer the newest pending call by id
👉🖥 انقر على إعادة تحميل adk web: يتبع function_response المكالمة المعلّقة، ويتم إصدار join_media، ويتم تشغيل store_release. تتحقّق ثمانية صفوف من المحطتَين في الجلسة: مكالمة البوابة وإجابتك، وإيصال العرض وتسليمه.
الولاية والمكتب (5ب)
مكتب الوسائط هو بنية 3a مرة أخرى، مع تفرع وانضمام، مع فرع واحد بطيء. يرسل script_ready النص البرمجي بطريقتَين. يُرسل "render_desk" المقطع ويتم تعليق عملية التشغيل. make_poster هي أداة الجلسة 2 كعقدة: يتم إرسال poster_idea الخاص بالنص البرمجي وأسلوب القناة وما يعرفه Memory Bank عن ذوق صانع المحتوى في الملصقات (مرتبط بالحالة كـ memory_facts، الجلسة 6) إلى نموذج الصور، ويتم عرض ملف PNG بنسبة 9:16 في ثوانٍ. ينتظر join_media الجهاز البطيء، ويكتب store_release ما وجده في الحالة.
المشاركة حسب الاسم: store_release لا يتلقّى الملصق مطلقًا. وتُعلن عن مَعلمة باسم poster_url، وتربطها حزمة ADK من الحالة المشتركة، لأنّ عقدة في الفرع الآخر كتبت هذا المفتاح. لا يتم نقل أي حمولة بين الفروع:
def store_release(node_input, poster_url: str = "", poster_path: str = ""):
"""`poster_url` and `poster_path` are not passed by anyone: ADK binds them
from shared STATE because the parameter names match the keys make_poster
wrote (the STATE_SHARE edit). The two branches of the desk never hand each
other payloads; they share what they made by name."""
render = state.load().get("render") or {} # the delivered clip, written by deliver
url, status = render.get("url") or "", render.get("status", "")
...
if not poster_url:
said += " · poster: not in state (did make_poster write poster_url to state? the STATE_SHARE edit)"
yield Event(output={"status": status, "url": url, "poster_url": poster_url},
state={"render_url": url, "render_status": status, "poster_url": poster_url},
message=f"video: {said} · poster: {poster_url or 'none'}")
As shipped, make_poster yields the poster as output and a message, and store_release says poster: not in state. 👉🖥 أضِف الحالة إلى الناتج النهائي:
state={"poster_url": rec.get("url", ""), "poster_path": rec.get("path", "")},
message=f"poster: {said}")
👉🖥 شغِّل البرنامج مرة أخرى وسلِّم المشروع. افتح حدث "make_poster": عنوان URL الخاص بالملصق في الناتج وفرق الحالة مع "poster_url". افتح store_release: تذكر رسالتها اسم الناشر. يعرض قسم جلسة هذا التشغيل في الصفحة الصف نفسه الذي تعرضه علامة التبويب "الحالة" في adk web: معرّف الجلسة الذي يحتاجه الاستئناف، والمكالمة المفتوحة، والمفاتيح التي كتبتها العُقد، ومن بينها poster_url. تعرض لوحة التحقّق المقطع والملصق جنبًا إلى جنب.

8. 6 · الذاكرة: ما تتذكّره القناة بين الليالي

👉🖥 في طاولة العمل، افتح الجلسة 6: الذاكرة، الجزأين 6أ و6ب، اللذين تم إنشاؤهما في stage4_memory. تناولت الجلسة الخامسة حالة تشغيل واحد. تتناول هذه الجلسة ما تتذكّره القناة بين عمليات التشغيل: ما يسمعه الرسم البياني من الخارج، من خلال عمليتَي استدعاء.
Memory Bank (6a)
Vertex AI Memory Bank هو ذاكرة طويلة المدى مُدارة حول شخص معيّن. تندرج الحقائق ضمن نطاق معيّن، واسم التطبيق ورقم تعريف المستخدم، وضمن المواضيع التي تحدّدها. عندما تزوّده بمحادثة مع "memories.generate"، يستخرج منها ما يناسب موضوعًا معيّنًا، ويضمّنه، ويدمجه مع ما لديه من معلومات: يتم إنشاء حقيقة جديدة، وتحديث حقيقة تم تغييرها، ودمج حقيقة مكرّرة. تعرض الدالة memories.retrieve الحقائق الخاصة بنطاق معيّن. يتم إجراء المكالمتَين في agent/platform/memory.py.
SCOPE = {"app_name": config.APP, "user_id": config.USER}
TOPICS = {
"CREATOR_TASTE": "Which video directions this creator picks and passes on, "
"and how that preference changes over time.",
"CHANNEL_RULES": "Standing instructions the creator states for every video "
"(style, subjects to avoid, format rules).",
"VISUAL_STYLE": "What this creator approves and rejects in a poster: palettes, "
"framing, props and mood of the posters they shipped.",
}
الموضوع الثالث هو ذوق القناة في الصور. كتب الإصدار في الجلسة 5: يتم وصف ملصق تم شحنه في جملة واحدة وتذكّره، ويتم رسم الملصق التالي باستخدام تلك الجملة في طلبه.
بدأ الإعداد في إنشاء البنك، ويشير صف الاتصال في الصفحة 6a إلى ما إذا كان متاحًا. من أزرار الصفحة أو من الوحدة الطرفية: ينشئ python -m agent.platform.bank البنك ومواضيعه ويخزّن اسم المورد مؤقتًا في runs/memorybank.json؛ يضيف python -m agent.platform.bank load أربع جلسات سابقة (عصران للحيوانات مع قاعدة نمط، وعصر للأدوات، وعصر خيالي حديث)؛ يوضح python -m agent.platform.bank list ما أصبحت عليه النصوص: بيانات موحدة للحقائق، وليس نصوصًا.
عمليات معاودة الاتصال (6b)
الذاكرة ليست عقدة. عملية ردّ الاتصال هي دالة يتم تمريرها إلى Agent، ويستدعيها ADK في لحظة ثابتة مع السياق المباشر، ويؤدي عرض None إلى حدوث الإجراء العادي. ثلاثة أزواج: حول دور الوكيل (before_agent_callback وafter_agent_callback مع CallbackContext)، وحول كل طلب نموذج (before_model_callback مع LlmRequest وafter_model_callback مع LlmResponse)، وحول كل طلب أداة. يتم هنا وضع الإجراءات الوقائية والقياس عن بُعد وذاكرة التخزين المؤقت وإدخال السياق، بدون عقدة في الرسم البياني.
في stage4_memory/agent.py، يقرأ "propose_directions" قاعدة البيانات قبل طلب النموذج: يجلب "recall_taste" الحقائق، ويُلحقها بالطلب ككتلة "ذاكرة"، بدءًا من الأقدم، ويطلب من النموذج توجيه المرشحين من 1 إلى 3 نحو الذوق الحالي والتعامل مع القواعد كقيود. ويكتب أيضًا الحقائق التي يجب ذكرها على شكل memory_facts، حيث يعثر عليها make_poster. 👉🖥 التوصيل:
output_schema=Directions,
before_model_callback=recall_taste)
يكتب كاتب النص ما اختاره صانع المحتوى بعد دوره: يقرأ "remember_pick" "direction" من الحالة، ويكتب جملة واحدة عن الاختيار، ويسلّمها إلى "memories.generate". 👉🖥 التوصيل:
output_schema=Script,
after_agent_callback=remember_pick)
👉🖥 أرسِل رسالة فارغة واختَر بطاقة. افتح حدث propose_directions: يتضمّن الطلب الفترات الثلاث، ويميل المرشحون من 1 إلى 3 إلى الذوق الحالي بينما تشير المؤشرات إلى شيء آخر. بعد كتابة النص، انقر على عرض سجلّ التعليقات: تم تغيير تعليق أو إضافته لاختيار الليلة. شغِّلها لليلة ثانية: ستتذكر الليلة الأولى.
9. 7 · النشر والإطلاق: الرسم البياني داخل التطبيق، ليلة على VibeTube

👉🖥 في workbench، افتح الجلسة 7: النشر والإصدار، الجزأين 7أ و7ب.
نشر VibeStudio (7أ)
في بيئات الاختبار المعزولة، كان adk web هو المحرّك الأساسي للرسم البياني. يستخدم VibeStudio حزمة ADK من Runner في سلسلة الوحدات العاملة (worker thread) مع حلقة معالجة الأحداث الخاصة بها، ويجمع كل حدث في حالة تشغيل واحدة تقرأها الصفحة عبر "الأحداث المُرسَلة من الخادم" (Server-Sent Events): القرّاء، واختيارك لأربع بطاقات، والملصق عند وصوله، والعرض عند تسليمه. تتم الإجابة عن طلب البوابة وتسليم العرض في المكالمة نفسها، وهي جزء من function_response، ويستطلع التطبيق Veo نفسه بدلاً من انتظار وحدة التحكّم.
self._svc = DatabaseSessionService(db_url=config.DB_URL)
self._runner = Runner(app_name=config.APP, agent=wf, session_service=self._svc)
async for ev in self._runner.run_async(user_id=config.USER, session_id=run_id, new_message=message):
self._absorb(ev) # fold the ADK event into the run state, publish one app event
part = Part(function_response=FunctionResponse(id=call_id, name=name, response=response))
vibestudio/server/agent/ هي نسخة مطابقة تمامًا من agent/ في التمرين، تم التحقّق منها بواسطة checks/verify_app.py. قائمة الحواف الكاملة في agent/graph.py هي كل بنية في هذا الدرس التطبيقي حول الترميز في مكان واحد، ثلاثة عشر عقدة؛ يتم شحنها ممتلئة، لتقرأها، وليس للكتابة:
(START, scan_trends, join_research),
(START, read_backlog, join_research),
(START, read_feedback, join_research),
(join_research, propose_directions, direction_gate,
persist_direction, policy_check),
(policy_check, {"OK": scripter, "BLOCK": quarantine}),
(quarantine, scripter),
(scripter, script_ready),
(script_ready, render_desk, join_media),
(script_ready, make_poster, join_media),
(join_media, store_release),
👉🖥 انشروا التطبيق باستخدام زر نشر في workbench، أو 👉💻 من علامة التبويب 1:
cd ~/vibe-studio-multimodal
uv run python vibestudio/deploy.py
الذي يتم تشغيله، مع ملء القيم من .env:
gcloud run deploy vibestudio --source vibestudio \
--project $GOOGLE_CLOUD_PROJECT --region us-central1 \
--labels dev-tutorial-codelab=vibetube --allow-unauthenticated \
--memory 2Gi --cpu 2 --timeout 3600 --concurrency 40 \
--max-instances 1 --min-instances 1 --session-affinity \
--set-env-vars GOOGLE_CLOUD_PROJECT=...,STUDIO_VERTEX=1,STUDIO_MEMORY_BANK=...,STUDIO_RAG_CORPUS=...,VIBETUBE_URL=...,VIBETUBE_EVENT=...,VIBETUBE_NAME=...,VIBETUBE_PROJECT=...
ينشئ --source الحاوية باستخدام Cloud Build وينشرها في خطوة واحدة، ويستغرق الإصدار الأول أربع أو خمس دقائق. يحتفظ --session-affinity مع مثيل دافئ واحد بتشغيل معلّق على المثيل الذي يحتفظ به: مخزن الجلسة للجلسة 5، مرة أخرى. في عمليات النشر الأكبر حجمًا، ينتقل مخزن الجلسات إلى VertexAiSessionService وتختفي الألفة. يتم تشغيل عملية الإنشاء والخدمة كحساب خدمة Compute Engine التلقائي للمشروع، والذي منحته عملية الإعداد الأدوار التي يحتاجها.
👉🖥 افتح عنوان URL للخدمة وشغِّل ليلة في التطبيق: فكرة، واختيار، وانتظر المقطع والملصق. وتحتفظ علامة التبويب "السجلّ" بكليهما.
النشر (7b)
تتم عملية النشر من خلال طلب POST واحد متعدد الأجزاء من التطبيق إلى نقطة نهاية الغرفة على VibeTube، بتنسيق vibestudio/server/platform/publish.py. المقطع هو الفيديو، والملصق هو الصورة المصغّرة، والملف الشخصي يحدّد اسم الغرفة واسمك:
url = f"{prof['platform_url']}/api/events/{prof['event_code']}/videos"
data = {"title": state.script["title"], "description": state.script["description"],
"duration": "0:08", "displayName": prof["display_name"],
"projectId": prof["project_id"]} # one video per project and room: publishing again replaces it
files = {"videoFile": (clip.name, clip.open("rb"), "video/mp4"),
"thumbnailFile": (poster.name, poster.open("rb"), "image/png")} # the poster, not a frame
r = httpx.post(url, data=data, files=files, timeout=120)
video_id = r.json()["id"] # watch it at {platform}/e/{event}?v={video_id}
يحتفظ تطبيق VibeTube بفيديو واحد لكل projectId وغرفة، والمشروع الذي يرسله التطبيق هو مشروعك على Google Cloud، لذا يؤدي النشر مرة أخرى إلى استبدال الفيديو بدلاً من إضافة فيديو جديد. لا يتم تحميل ملصق بديل، بل يتم استخدام ملصق حقيقي كصورة مصغّرة، بايت ببايت.
التعديل الأخير هو إعداد، وليس سطرًا برمجيًا. 👉🖥 في الصفحة 7b: عنوان URL الخاص بـ VibeStudio (الذي تم ملؤه من عملية النشر في 7a)، ورمز الحدث الخاص بالغرفة، تمامًا كما يكتبه VibeTube (الذي أدخلته في الإعداد، ما لم يفتح معلّمك رمزًا آخر)، واسمك. انقر على نشر. ينقل طاولة العمل النقرة إلى التطبيق، وينشر التطبيق الليلة، وتقرأ الصفوف أدناه النتيجة من التطبيق ومن VibeTube: يجيب التطبيق، وتنتهي الليلة بملصق حقيقي، ويسمي الرمز غرفة مفتوحة، وأعاد VibeTube معرف فيديو، والفيديو على الحائط، وصورته المصغرة هي الملصق الخاص بك. تعرض بطاقة النتيجة الصورة المصغّرة على VibeTube بجانب الملصق في التطبيق.

10. ما أنشأته
👉🖥 يعرض الجزء الأخير من الصفحة 7b الرسم البياني بأكمله، وهو عبارة عن ثلاثة عشر عقدة من فكرة إلى مقطع وملصق؛ مرّر مؤشر الماوس فوق عقدة لمعرفة ما تفعله والجلسة التي أنشأتها.
الجلسة | ما تعلّمته | تُكتب على النحو التالي: |
2 · إنشاء الصور | الوكيل: النموذج والتعليمات والأدوات، نموذج الصور كأداة، ما لا يمكن أن يضمنه الطلب | |
3 · سير العمل | قائمة الحواف، والتشعّب والدمج، وجهاز توجيه مع حلقة وضع المهام خلفه |
|
4 · استيعاب البيانات المتعددة الوسائط | قارئ واحد لكل نوع، وشكل خط واحد، ومجموعة واحدة، وقارئ ثالث حسب المعنى، مع الوسائط | |
5 · إنشاء الفيديوهات | نوعان من التعليق، آلية واحدة، مخزن الجلسات، الملصق الذي تمت مشاركته بالاسم | |
6 · الذاكرة | الذاكرة على مدار الليالي، استدعاءان | |
7 · النشر والإتاحة | الرسم البياني داخل تطبيق على Cloud Run، والليل على VibeTube | |
خمس قواعد يتبعها الرسم البياني:
- تعليق بدلاً من الانتظار: الإيقاف المؤقت هو صف في مخزن الجلسات، وليس سلسلة محادثات.
- استئناف كل شيء بالطريقة نفسها: من خلال رقم تعريف المكالمة المفتوحة، سواء ردّ شخص أو عملية.
- مشاركة البيانات حسب الاسم في الحالة، وليس من خلال ربطها بكل حمولة.
- اتّخِذ قرارًا باستخدام الرمز قبل الدفع مقابل نموذج. السياسة هي ملف نصي وموجّه، ويتم تنفيذها قبل العرض.
- قراءة كل ما هو ليس نصًا في نص قبل أن يصل إلى المجموعة، مع اسم الملف داخل السطر حتى يتمكن الملف من العودة.
لا يمكن أن يَعِد الطلب بترتيب أو توقّف أو تحقّق. أما الرسم البياني، فيمكنه ذلك.
الخطوات التالية
VertexAiSessionServiceللجلسات التي تستمر بعد انتهاء مثيل، وإسقاط--session-affinity.- Webhook يجيب على طلب العرض بدلاً من أداة الاستطلاع:
FunctionResponse(id=...)نفسه، ولكن المُرسِل مختلف. - ثانية واحدة
RequestInputقبل النشر: المخطط هو النموذج، لذا فهو عقدة واحدة ولا يوجد رمز صفحة. - تتم إضافة التعليقات والملاحظات الصوتية والمقاطع الجديدة التي يرسلها الجمهور إلى المجموعة بعد كل إصدار، مع الملصق الذي تم إرسالها تحته.
تَنظيم
كل ما تم إنشاؤه في هذا التمرين المعملي موجود في المشروع الذي تم إنشاؤه في الخطوة 4، لذا فإنّ أرخص عملية تنظيف هي حذف المشروع. في Cloud Shell:
gcloud projects delete $(cat ~/project_id.txt)
للاحتفاظ بالمشروع وإزالة الأجزاء المدفوعة فقط: خدمة Cloud Run (gcloud run services delete vibestudio --region us-central1)، ومجموعة بيانات RAG وMemory Bank (أسماء الموارد الخاصة بهما في runs/ragcorpus.json وruns/memorybank.json، وتحذفهما صفحتا Agent Engine وRAG Engine في وحدة التحكّم)، وصورة الحاوية في Artifact Registry.