1. Введение

Вы ведете канал на VibeTube, где публикуете уютные, но жутковатые видео. До Хеллоуина вы планируете выкладывать по одному короткому ролику каждый вечер. Аудитория хочет видеть маленькое волшебство в маленьких комнатах и отправляет его вам всеми возможными способами – в виде комментариев, голосовых заметок, фотографий собственных ящиков с привидениями и клипов со своего крыльца в сумерках. Бэклог полон идей. Каждый вечер уходит на одни и те же дела: изучить, что сейчас в тренде, прочесать бэклог, прочитать, что прислали зрители, выбрать направление, проверить его на соответствие правилам канала, написать сценарий, нарисовать постер, отрендерить клип, опубликовать.
Генеративные модели могут выполнять каждую задачу по отдельности. Gemini ищет информацию, слушает, смотрит и пишет, модель для работы с изображениями рисует, а Veo рендерит. Но они не могут самостоятельно провести ночную смену по порядку, остановиться, чтобы вы приняли решение, отказаться от плохого направления, пока оно не привело к убыткам, подождать несколько минут, пока идет рендеринг, не держа ничего открытым, и оставить запись.
В этой практической работе мы превратим такой вечер в ADK Workflow – нарисованный граф узлов и ребер, который запускается сам, спрашивает вас только там, где нужно ваше суждение, и заканчивает каждый запуск клипом и его постером на VibeTube.
Запрос не может гарантировать порядок, остановку или проверку. График может.
Что вы узнаете
- Один агент и модель генерации изображений в качестве инструмента.
Agent– это модель, инструкция и инструменты. Третий инструмент вызывает модель изображения, и первый запуск заканчивается постером. Затем предложение, которое определяет остальную часть лабораторной работы: запрос не может обещать заказ, остановку или проверку. - Структуры, из которых состоит граф.
Workflow– это список ребер. Вы создаете разветвление и объединение (параллельные считыватели, встречающиеся вJoinNode) и маршрутизатор, который называет свой собственный выход, с агентом в режиме задачи за отклоненным выходом, который очищает направление в цикле вызовов инструментов и снова присоединяется к основной линии. - Мультимодальный конвейер приема данных. Четыре типа контента, отправленного аудиторией, по одному читателю на каждый тип: Gemini расшифровывает голосовые заметки, подписывает фотографии, смотрит клипы и слушает их саундтреки, и каждый читатель записывает одну и ту же форму строки в один корпус RAG Engine. Третий читатель в разветвлении извлекает информацию по смыслу и возвращает медиафайлы вместе с отрывками.
- Два типа блокировки, один механизм. Прогон останавливается, когда нужно участие человека (
RequestInput: четыре карточки, вы отвечаете) и когда инструмент работает медленно (Veo заLongRunningFunctionTool: чек). Оба варианта означают одно и то же: открытый вызов в хранилище сеансов, возобновленный по его идентификатору из формы или другого процесса. Два филиала медиаагентства делятся постером по названию, в штате. - Память в течение ночи. Два обратных вызова жизненного цикла в банке памяти:
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 Edition of VibeStudio. Это мультимодальная версия, в которой основное внимание уделяется тому, что прислали зрители, и двум видам приостановки. Она представлена в одностраничном интерактивном формате Agent Valley labs. Студия – это декорации. Все механизмы под ней – обычные, которые вы использовали бы для конвейера поддержки, процесса проверки контента или чего-либо ещё, что должно приостанавливаться для человека и ждать медленной работы.
2. 🧰 Настройка
- Что – аккаунт, с которого можно оплачивать, проект, установленная лаборатория, модель, которая может отвечать, и рабочая среда, работающая в фоновом режиме на одном порту.
- Почему? Один процесс на одном порту: страница, редактор, верификаторы,
/api/*иadk webиспользуют одно и то же происхождение, поэтому для этой практической работы не нужен второй терминал или второй порт. - Как – клонировать, два скрипта. Второй блок выполняет реальный вызов модели, запускает рабочую среду и начинает создавать корпус RAG и банк памяти, пока вы читаете, чтобы они были готовы к сеансам 4 и 6.
Все действия в этой лабораторной работе выполняются в Cloud Shell. В этой среде уже установлены gcloud, Python, Node, ffmpeg и git. Доступ к Gemini, модели изображений, Veo, RAG Engine и Memory Bank осуществляется через Vertex AI с использованием ваших учетных данных. В этой лабораторной работе нет ключа API и не нужно ничего устанавливать на ноутбук. Ноутбук с Google Cloud SDK, Python 3.12 и Node 20+ работает так же.
Единственное, что не может предоставить эта лабораторная работа, – это проект Google Cloud с включенными платежными функциями. На шаге 1 вы создаете аккаунт, который может оплачивать проект, а на шаге 4 – превращаете этот аккаунт в проект. Выполняйте их в указанном порядке. Все шаги после четвертого относятся к проекту, выбранному на четвертом шаге.
1. Создайте аккаунт с платежными данными (~1 мин.)
Войдите в аккаунт Google, в котором включены платежные функции. Подойдет любой такой аккаунт. Если у вас нет такого аккаунта, начните бесплатный пробный период. Этого достаточно для выполнения шага 4. Если вы проходите лабораторную работу под руководством инструктора, сначала активируйте бонусы для этого сеанса – так вы создадите платежный аккаунт.
2. Откройте Cloud Shell (~2 мин.)
👉🖥 В верхней части консоли Google Cloud нажмите Активировать Cloud Shell (значок в виде терминала в заголовке).

👉🖥 Нажмите Открыть редактор (значок папки с карандашом). Откроется редактор Cloud Shell с проводником слева. В нем можно просматривать файлы целиком.

👉🖥 Если в какой-то момент вам будет предложено выполнить авторизацию, нажмите Авторизовать.
👉🖥 Откройте терминал в редакторе – это вкладка 1, и каждый 👉💻 шаг выполняется там. Вкладка 2 – это сама рабочая область, которую вы откроете на шаге 7. Если терминал не открыт в нижней части экрана, откройте его, выбрав View (Вид) → Terminal (Терминал).

👉💻 Проверьте, в какой аккаунт вы вошли. Это должен быть тот же аккаунт, что и на шаге 1.
gcloud auth list
3. Получите лабораторную работу (~1 мин.)
👉💻 Клонируйте репозиторий:
cd ~
git clone https://github.com/cuppibla/vibe-studio-multimodal
cd vibe-studio-multimodal
4. Создайте проект (~2 мин.)
👉💻 Один скрипт находит ваш платежный аккаунт, создает в нем проект, связывает платежи и указывает Cloud Shell на этот проект:
cd ~/vibe-studio-multimodal
./setup_project.sh
Вы должны увидеть ✓ для выбранного платежного аккаунта, ✓ для созданного проекта и сообщение, похожее на следующее:
Project ready: vibe-studio-4271 Next: ./setup_codelab.sh
vibe-studio-4271 – это идентификатор проекта. У вас он будет другим. В нем происходит четыре вещи:
billing | выбирает самый новый аккаунт | |
project | создает | |
выбирает | записывает идентификатор в | |
waits | опрашивает, пока проект не будет разрешено показывать (до двух минут). | |
👉🖥 Теперь этот проект есть в консоли. Его идентификатор указан на карточке Информация о проекте на панели управления.

5. Установка и настройка лаборатории (~4 мин.)
👉💻
cd ~/vibe-studio-multimodal
./setup_codelab.sh
Один скрипт, который можно безопасно запустить повторно. При этом сохранятся все ответы, которые вы дали в первый раз. В начале процесса вам будет задано два вопроса. Нажатие клавиши Ввод выбирает вариант по умолчанию для обоих.
код события | комнату VibeTube, в которой будет опубликовано ваше видео в сеансе 7. Введите код комнаты, полученный от преподавателя, или нажмите Ввод, чтобы перейти в комнату | |
отображаемое название | имя, которое вы указали в комнате, или нажмите Ввод, чтобы использовать имя из аккаунта. | |
Оба варианта сохраняются в .env, и их можно изменить позже в .env или на странице публикации. Затем он выполняет следующие действия:
uv env | устанавливает uv, если он отсутствует, а затем | |
apis | включает шесть API в проекте, выбранном на шаге 4, и подтверждает каждый ответ, прежде чем продолжить: | |
account | убедитесь, что в проекте есть сервисный аккаунт Compute Engine по умолчанию и у него есть роли, необходимые для сборки и сервиса сеанса 7. | |
.env | создает файл | |
veo · room | подтверждает, что | |
model | выполняет один реальный вызов Gemini, чтобы неудачный проект завершился сбоем на этапе настройки, а не во время сеанса. | |
starter | помещает девять файлов, которые вы будете редактировать, в | |
page | устанавливает и собирает | |
облако | начинает создавать корпус RAG Engine и банк памяти в фоновом режиме, чтобы они были готовы к сеансам 4 и 6. | |
preflight | запускает | |
6. Прочитайте информацию о предполетной подготовке (~1 мин.)
Последнее, что вывела команда на шаге 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) прав: этот график пуст, и вы заполните его на третьем занятии. Две строки - тоже правильные: корпус появляется через минуту или две, а банк – примерно через 20 секунд. Ни то, ни другое не понадобится вам до четвертого занятия.
👉💻 Запустите его снова в любой момент, чтобы увидеть, где вы находитесь — он считывает ваши файлы, ваш проект и работающий сервер, а не контрольный список:
cd ~/vibe-studio-multimodal
uv run python scripts/preflight.py
7. Откройте рабочую область (~1 мин.)
Рабочая область уже запущена. На шаге 5 мы запустили его в фоновом режиме. Один процесс uvicorn на порту 4600 обслуживает страницу, каждый маршрут /api/* и adk web, смонтированный в /workbench. Вам не нужно ничего запускать или перезапускать. Изменения будут применены при следующем запуске.
👉🖥 Нажмите на ссылку, которая появилась в результате предварительной проверки, или в Cloud Shell выберите Web Preview (Предварительный просмотр) → Change port (Изменить порт) → 4600. Назовем ее вкладка 2. Откроется раздел Занятие 1. История.
Три команды, которые могут вам понадобиться позже, все из ~/vibe-studio-multimodal:
| Останавливает рабочую среду. | |
| запускает его на переднем плане; 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 – сгенерировать видео. Человеку остается только выбрать и проверить.
Под кольцом находится график, который строит остальная часть лаборатории, тринадцать узлов от идеи до клипа и его постера. Наведите указатель на узел, чтобы узнать, что он делает и в каком сеансе создается. Здесь ничего не нужно редактировать. Кнопка Создать первый постер на странице – это сеанс 2.
Словарь, чтобы следующие шесть страниц читались без остановки:
слово | что это значит |
agent | ADK |
node | один шаг графа – функция Python, агент, объединение, маршрутизатор; |
edge | кортеж узлов, которые выполняются по порядку; граф представляет собой список таких кортежей. Сеанс 3 |
session | запись одного запуска в |
state | словарь, который узлы используют совместно по имени ключа; узел записывает |
memory | что сохраняется после сеанса – банк памяти, через обратные вызовы. Мероприятие 6 |
4. 2. Генерация изображений – один агент и модель изображений в качестве инструмента.

👉🖥 На рабочем столе откройте Session 2 · Image generation (Сеанс 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.
Практическое использование: одно изменение
В исходном виде make_poster возвращает словарь, в котором указано, что постер был пропущен. 👉🖥 В редакторе Workbench замените строку TODO на вызов модели изображений:
from agent.platform import images
return images.generate(subject, name="solo")
Сохранить. Строки проверки считывают файл: три инструмента в агенте, вызов модели изображения на месте. 👉🖥 Нажмите Проверить загрузку рабочего процесса, затем Открыть веб-версию ADK и отправьте:
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. Рабочие процессы: разветвление и объединение, маршрутизатор и цикл

👉🖥 На рабочей панели откройте Session 3 · Workflows (Сеанс 3. Рабочие процессы), части 3a и 3b. Каждая часть создается в приложении-песочнице, поэтому вы можете запустить ее в adk web отдельно: разветвление в stage1_fanout, маршрутизатор и его цикл в stage3_router.
Разветвление и объединение (3a)
ADK Workflow – это список ребер. Каждая запись представляет собой цепочку – кортеж узлов, которые выполняются по порядку. Две цепочки, выходящие из START, выполняются параллельно. Цепочка, которая заканчивается на JoinNode, сообщает об этом, и объединение ожидает каждую цепочку, которая заканчивается там, прежде чем продолжить, передавая один словарь, ключом которого является имя узла. Цель, записанная в виде словаря, сопоставляет имена маршрутов маршрутизатора с узлами. На странице 3a верстака рисуются фигуры и есть упражнение на запись ребер.
Тип узла | Записано как | Описание |
Узел функции |
| Простой 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 – одна строка, описывающая изображение, которое продает видео. Сценарист сохраняет ее, а медиаотдел рисует на пятом занятии.
Маршрутизатор – это узел функции, возвращаемое значение которого определяет ребро, по которому будет выполняться запуск. Без вызова модели: 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. Четвертый вариант – это приманка. policy_check выдает BLOCK, карантин вызывает свои инструменты, переписывает запрещенные слова, вызывает finish_task, и очищенная инструкция возвращается к сценаристу. Строки проверки считывают оба запуска из хранилища сеансов.
Каждое предложение запроса из второго сеанса теперь имеет узел: исследование – это два читателя и объединение, отказ – это маршрутизатор и агент задач, скрипт – это узел агента. На всем графике заменяющий выбор – это два узла функций, direction_gate и persist_direction, которые, как вы увидите, выполняются до тех пор, пока сеанс 5 не остановит дверь. Соглашение с автором пока не заключено, а исследование не учитывает мнение аудитории.
6. 4. Мультимодальная загрузка. Все, что отправляет аудитория, считывается в один корпус.

👉🖥 На рабочей панели откройте Session 4 · Multimodal ingestion (Сеанс 4. Мультимодальная загрузка), части 4a и 4b. Зрители не только писали. Один считыватель для каждого типа преобразует все отправленные данные в текст одинаковой формы, один корпус RAG Engine хранит все это, а третий считыватель в разветвлении ищет его по значению и возвращает медиафайлы.
Что отправила аудитория (4a)
agent/comments.md содержит 12 комментариев, оставленных под шестью видео на канале, посвященными Хеллоуину. agent/audience/ содержит остальное: восемь голосовых заметок (voice/*.mp3), четырнадцать фотографий комнат зрителей (photo/*.jpg) и шесть клипов (clip/*.mp4) – медленное панорамирование по фотографии с голосом зрителя за кадром. На странице 4a открывается все: воспроизводятся все голосовые заметки, показываются все фотографии, запускаются все клипы.

RAG Engine извлекает текст. Голосовая заметка, фотография или видеоклип могут присоединиться к корпусу только так, как это делали плакаты в 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 Engine – это система поиска по документам. Корпус разбивается на фрагменты примерно по 120 токенов, каждый фрагмент встраивается с помощью text-embedding-005, а запрос встраивается таким же образом и получает ответ с ближайшими по смыслу, а не по общим словам фрагментами. "уютные маленькие призраки дома" находит комментарий с просьбой о маленьком волшебстве в маленьких комнатах, голосовую заметку о летучей мыши, обнимающей лампочку, и клип, в саундтреке которого говорится, что призрак – это бумажный стаканчик.
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" показывает фрагменты, найденные при поиске, которые не содержат ни одного слова из запроса.
The edit. В стандартной комплектации READERS содержит только read_comments. Три устройства чтения карт памяти находятся за одним #. 👉🖥 Запустите импорт один раз, как он есть. В выходных данных будет указано not read (no reader wired in audience.READERS): voice, photo, clip, и будут импортированы только комментарии и субтитры. Затем удалите # :
transcribe_voice, caption_photos, watch_clips,
👉🖥 Сохраните изменения и запустите импорт снова. Чтение 40 элементов занимает около трех минут. В консоли по очереди появляются строки heard, saw и watched, а каждая карточка в сетке заполняется соответствующей строкой. Строки проверки считывают файл, выходные данные читателей и корпус: подключено четыре читателя, прослушано восемь голосовых заметок, просмотрено четырнадцать фотографий, просмотрено шесть клипов, пять файлов в корпусе.
Третий читатель (4b)
Отзывы аудитории – это исследование, которое передается каждым запуском, поэтому оно попадает в граф там же, где и исследование: как третий читатель в соединение. read_feedback в agent/graph.py берет сегодняшнюю идею, запрашивает у корпуса ближайшие отрывки и сообщает join_research, которая ждет три. Ребро (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 с идеей. Open read_feedback: запрос – это ваша идея, а выходные данные – пять ближайших фрагментов и список media с URL для каждого файла. Открыть join_research: три клавиши. Откройте propose_directions: кандидаты 1–3 цитируют feedback в своих доказательствах. На панели проверки показано, что читатель посмотрел под строками: клип, три голосовые заметки и постер с призраком в свете холодильника.

7. 5. Генерация видео: два вида блокировки, один механизм

👉🖥 В рабочей области откройте Session 5 · Video generation (Сеанс 5. Создание видео), части 5a и 5b, созданные в stage6_video, весь граф. Клип занимает минуты, а постер – секунды, и пробежка останавливается дважды по пути: один раз для вас, один раз для Veo. Две остановки – это один механизм, и две ветви стола делят то, что они сделали, по названию.
Два типа блокировки (5a)
Ничего не ждет. Если выполнение прерывается, в сеанс записывается открытый вызов, и выполнение завершается. Тот, кто отвечает на вызов (человек или процесс), продолжает выполнение. На странице будет одна таблица:
на человека · | для медленного инструмента · | |
который останавливает выполнение |
|
|
чего он ждет | человек: одна из четырех карточек | медленная задача: работа Veo, минуты |
что находится в сеансе | открытый вызов | открытый вызов |
как возобновить |
|
|
кто отправляет; |
|
|
где он живет в настоящее время |
| одна строка, один идентификатор – ничего не выполняется; |

Для физических лиц. В одном запросе "попросите автора подтвердить" – это рекомендация, и во втором сеансе она была проигнорирована в одном сообщении. В рабочем процессе пауза – это двигатель. Узел, который возвращает 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 запускается. Восемь строк проверки считывают две остановки из сеанса: вызов шлюза и ваш ответ, получение рендера и его доставку.
Укажите и рабочий стол (5b)
Медиа-стол снова представляет собой структуру 3a, разветвление и объединение с одной медленной ветвью. script_ready отправляет скрипт двумя способами. render_desk отправляет клип, и выполнение приостанавливается. make_poster – это инструмент из второго занятия, представленный в виде узла. Сценарий (poster_idea), стиль канала и информация о предпочтениях автора в отношении постеров (связанная с состоянием как 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'}")
В исходном виде make_poster выдает постер и сообщение, а store_release – 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 · Память — что канал помнит между ночами

👉🖥 В рабочей среде откройте Session 6 · Memory (Сеанс 6 · Память), части 6a и 6b, созданные в 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.",
}
Третья тема – вкус канала в картинках. В пятом сеансе в него была записана информация: один из отправленных постеров был описан одним предложением, которое было запомнено, а следующий постер был нарисован с использованием этого предложения в запросе.
Настройка уже начала создавать банк; в строке подключения на странице 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 извлекает факты, добавляет их к запросу в виде блока MEMORY (сначала самые старые) и сообщает модели, что варианты 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

👉🖥 В рабочей области откройте Session 7 · Deploy and publish (Сеанс 7. Развертывание и публикация), части 7a и 7b.
Развертывание VibeStudio (7a)
В песочницах график определялся adk web. VibeStudio управляет этим с помощью Runner ADK в рабочем потоке с собственным циклом событий и объединяет каждое событие в одно состояние выполнения, которое страница считывает через 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),
👉🖥 Нажмите кнопку Deploy (Развернуть) в рабочей области или 👉💻 на вкладке 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 показан весь граф, состоящий из 13 узлов, от идеи до клипа и постера. Наведите указатель на узел, чтобы узнать, что он делает и в каком сеансе он был создан.
Сеанс | Чему он научил | Записано как |
2 · Генерирование изображений | Агент: модель, инструкции, инструменты; модель изображений как инструмент; что не может обещать запрос |
|
3. Рабочие процессы | Список ребер, разветвление и объединение, маршрутизатор с циклом в режиме задачи за ним |
|
4. Мультимодальная загрузка | Один читатель на каждый вид, одна форма строки, один корпус; третий читатель по смыслу, с медиа |
|
5. Создание видео | Два вида блокировки, один механизм; хранилище сеансов; постер, которым поделились по имени |
|
6 · Память | Память в течение нескольких ночей, два обратных вызова |
|
7. Развертывание и публикация | График в приложении на Cloud Run; ночь на VibeTube |
|
Пять правил, которым следует график:
- Приостановка вместо ожидания. Пауза – это строка в хранилище сеансов, а не поток.
- Возобновить все так же – по идентификатору открытого вызова, независимо от того, отвечает ли человек или процесс.
- Передавайте данные по имени в состоянии, а не через каждую полезную нагрузку.
- Прежде чем платить за модель, проверьте ее с помощью кода. Правило представляет собой текстовый файл и маршрутизатор и выполняется до отрисовки.
- Преобразуйте все, что не является текстом, в текст, прежде чем оно попадет в корпус, указав название файла в строке, чтобы файл можно было вернуть.
Запрос не может гарантировать порядок, остановку или проверку. График может.
Что делать дальше
VertexAiSessionServiceдля сеансов, которые переживают экземпляр, и удалите--session-affinity.- Вебхук, который отвечает на вызов рендерера вместо опрашивающего: тот же
FunctionResponse(id=...), другой отправитель. - Второй
RequestInputперед публикацией – схема представляет собой форму, поэтому это один узел и нет кода страницы. - Новые комментарии, голосовые заметки и клипы, добавленные к корпусу после каждого релиза, с постером, под которым они были отправлены.
Организовать
Все ресурсы, созданные в ходе этой лабораторной работы, находятся в проекте, созданном на шаге 4, поэтому самый простой способ очистки – удалить проект. В Cloud Shell:
gcloud projects delete $(cat ~/project_id.txt)
Чтобы сохранить проект и удалить только платные компоненты: сервис Cloud Run (gcloud run services delete vibestudio --region us-central1), корпус RAG и банк памяти (их названия ресурсов указаны в runs/ragcorpus.json и runs/memorybank.json, и их можно удалить на страницах Agent Engine и RAG Engine в консоли), а также образ контейнера в Artifact Registry.