1. Introduzione

Gestisci un canale accogliente e spettrale su VibeTube: uno Short ogni sera fino ad Halloween. Il pubblico vuole piccole magie in piccole stanze e te le invia in ogni modo possibile: commenti, note vocali, foto dei propri cassetti infestati e clip del proprio portico al tramonto. Il backlog è pieno di idee. Ogni sera si dedica alle stesse attività: ricerca cosa sta spopolando nel feed, passa al setaccio i contenuti arretrati, legge ciò che il pubblico ha inviato, sceglie una direzione, la confronta con le norme del canale, scrive lo script, disegna il poster, esegue il rendering del clip e pubblica.
I modelli generativi possono svolgere ogni compito da soli. Gemini ricerca, ascolta, guarda e scrive; il modello di immagine disegna; Veo esegue il rendering. Quello che non possono fare da soli è gestire la notte in ordine, fermarsi per l'unica decisione che spetta a te, rifiutare una cattiva direzione prima che costi denaro, aspettare minuti per un rendering senza tenere aperto nulla e lasciare un record.
Questo codelab trasforma quella serata in un ADK Workflow: un grafico disegnato di nodi e archi che si esegue da solo, ti chiede solo dove è necessario il tuo giudizio e termina ogni esecuzione con un clip e il relativo poster su VibeTube.
Un prompt non può promettere ordine, un arresto o un controllo. Un grafico sì.
Cosa imparerai a fare
- Un agente e il modello di immagini come strumento. Un
Agentè un modello, un'istruzione e degli strumenti. Il terzo strumento chiama il modello di immagine e la prima esecuzione termina con un poster. Poi la frase che guida il resto del lab: un prompt non può promettere ordine, un arresto o un controllo. - Le strutture di cui è composto un grafico. Un
Workflowè un elenco di archi. Devi creare il fan-out e il join (lettori paralleli che si incontrano in unJoinNode) e un router che assegna un nome alla propria uscita, con un agente in modalità attività dietro l'uscita rifiutata che pulisce una direzione in un loop di chiamate di strumenti e si unisce alla linea principale. - Una pipeline di importazione multimodale. Quattro tipi di contenuti inviati dal pubblico, un lettore per tipo: Gemini trascrive le note vocali, aggiunge i sottotitoli alle foto, guarda i clip e ascolta le loro colonne sonore, e ogni lettore scrive la stessa forma di riga in un corpus di RAG Engine. Un terzo lettore nel fan-out recupera i contenuti in base al significato e restituisce i contenuti multimediali con i passaggi.
- Due tipi di sospensione, un unico meccanismo. Una corsa si ferma per una persona (
RequestInput: quattro carte, rispondi tu) e per uno strumento lento (Veo dietroLongRunningFunctionTool: una ricevuta). Entrambi sono la stessa cosa: una chiamata aperta nello store della sessione, ripresa dal suo ID, dal modulo o da un altro processo. Le due sezioni del media desk condividono il poster per nome, nello stato. - Memoria tra le notti. Due callback del ciclo di vita su Memory Bank:
before_model_callbackrichiama i gusti del creator prima che il suggeritore legga,after_agent_callbackricorda la scelta di stasera dopo lo scripter. - La release. Il grafico all'interno di un'app con un ADK
Runner, eventi inviati dal server alla pagina, Cloud Run con affinità di sessione e un POST multiparte che inserisce il clip su VibeTube con il poster come miniatura.
Cosa creerai
Un'app in esecuzione nel browser: VibeStudio Workbench, una pagina complementare con un editor di codice in-page, strumenti di verifica che leggono i file sul disco e le sessioni scritte dalle tue esecuzioni e un adk web incorporato. Le sette sessioni sono le sette pagine di questo codelab, con gli stessi numeri e le stesse lettere delle parti, in modo che i due rimangano sincronizzati. Ogni pagina si apre con una fascia che descrive la sessione in una frase e le tre azioni che puoi eseguire: modifica, esecuzione, verifica. Leggi una sezione qui, poi apporta la modifica nel workbench. Ogni modifica termina con un riquadro di verifica che mostra ciò che è effettivamente accaduto.
Vengono eseguiti tre elementi: Workbench (la pagina sulla porta 4600, l'editor, i verificatori), il tuo backend (Workflow in agent/ e un'app sandbox per sessione in stage0_prompt/ tramite stage6_video/, fornita da adk web) e Google Cloud (Gemini, il modello di immagine, RAG Engine, Veo, Memory Bank, tutti raggiungibili tramite Vertex AI con le tue credenziali).
Le sette sessioni
Sessione | Cosa creerai | Le tue modifiche |
1 · La storia | Ancora niente: la notte e il grafico che diventa | — |
2 · Generazione di immagini | Un agente disegna il primo poster |
|
3 · Workflows | La notte come un grafico: il fan-out e il join, il router e il suo loop |
|
4 · Importazione multimodale | What the audience sent, read into one corpus; the third reader |
|
5 · Generazione di video | Due tipi di sospensione; l'autore del post ha condiviso il nome nello stato |
|
6 · Memoria | Cosa ricorda il canale tra una notte e l'altra |
|
7 · Esegui il deployment e pubblica | VibeStudio su Cloud Run; la notte su VibeTube | il codice evento della stanza e pubblicare |
Come utilizzare questo codelab. È incentrato sui concetti: ogni sessione spiega l'idea, poi mostra il codice così come viene letto una volta apportata la modifica e lo illustra. Non ti viene mai chiesto di modificare un file qui: la modifica avviene nel workbench, che ti presenta una modifica alla volta, un suggerimento accanto e un riquadro di verifica sotto. Leggi la sessione, poi lascia che la pagina ti guidi.
Dove si trova
Il grafico, il tema e la stanza su VibeTube provengono dalla Haunted Edition di VibeStudio di Lin; questo è il taglio multimodale, incentrato su ciò che il pubblico ha inviato e sui due tipi di sospensione, nel formato interattivo di una pagina dei lab di Agent Valley. Lo studio è lo scenario; ogni meccanismo sottostante è quello ordinario che utilizzeresti per una pipeline di assistenza, un flusso di revisione dei contenuti o qualsiasi altra cosa che deve essere messa in pausa per una persona e attendere un job lento.
2. 🧰 Configurazione
- Cosa: un account che può pagare, un progetto, il lab installato, il modello che ha dimostrato di rispondere e il workbench in esecuzione in background su una porta.
- Perché: un processo su una porta. La pagina, l'editor, i verificatori,
/api/*eadk webcondividono un'origine, quindi non è necessario un secondo terminale o una seconda porta in questo lab. - Come: clonazione, due script. Il secondo effettua una vera e propria chiamata al modello, avvia il workbench e inizia a creare il corpus RAG e il Memory Bank mentre leggi, in modo che le sessioni 4 e 6 li trovino pronti.
Tutto ciò che viene eseguito in questo lab avviene in Cloud Shell: gcloud, Python, Node, ffmpeg e git sono già installati e Gemini, il modello di immagine, Veo, RAG Engine e Memory Bank sono tutti accessibili tramite Vertex AI utilizzando le tue credenziali. In questo lab non è presente alcuna chiave API e non è necessario installare nulla sul tuo laptop. Un laptop con Google Cloud SDK, Python 3.12 e Node 20+ funziona allo stesso modo.
C'è una cosa che questo lab non può fornirti: un progetto cloud Google con fatturazione attiva. Il passaggio 1 ti consente di ottenere un account che può pagare per il progetto, mentre il passaggio 4 trasforma l'account in un progetto. Eseguili in questo ordine: ogni passaggio successivo al 4 legge il progetto stabilito nel passaggio 4.
1 · Ottieni un account con fatturazione (~1 min)
Accedi con un Account Google per cui è abilitata la fatturazione. Qualsiasi account di questo tipo va bene. In caso contrario, avvia una prova senza costi, che ti fornisce un account di fatturazione, l'unico elemento necessario per il passaggio 4. Se stai partecipando a un lab guidato da un insegnante, utilizza prima i crediti per questa sessione, in modo da creare l'account di fatturazione.
2 · Apri Cloud Shell (~2 min)
👉🖥 Fai clic su Attiva Cloud Shell nella parte superiore della console Google Cloud: è l'icona a forma di terminale nell'intestazione.

👉🖥 Fai clic su Apri editor (l'icona della cartella aperta con una matita). Viene visualizzato l'editor di Cloud Shell con un file explorer sulla sinistra, che puoi utilizzare ogni volta che vuoi leggere un intero file.

👉🖥 Se ti viene chiesto di autorizzare in qualsiasi momento oggi, fai clic su Autorizza per continuare.
👉🖥 Apri un terminale all'interno dell'editor, ovvero la scheda 1, ed esegui ogni passaggio 👉💻 lì. La Scheda 2 è il workbench stesso, che aprirai nel passaggio 7. Se il terminale non è già nella parte inferiore dello schermo, aprilo con Visualizza → Terminale:

👉💻 Controlla con quale account hai eseguito l'accesso: deve essere quello del passaggio 1:
gcloud auth list
3 · Ottieni il lab (~1 min)
👉💻 Clona il repository:
cd ~
git clone https://github.com/cuppibla/vibe-studio-multimodal
cd vibe-studio-multimodal
4 · Crea il tuo progetto (~2 min)
👉💻 Uno script trova il tuo account di fatturazione, crea un progetto, collega la fatturazione e indirizza Cloud Shell a quel progetto:
cd ~/vibe-studio-multimodal
./setup_project.sh
Dovresti vedere un ✓ per l'account di fatturazione scelto, un ✓ per il progetto creato e un finale simile a questo:
Project ready: vibe-studio-4271 Next: ./setup_codelab.sh
vibe-studio-4271 è l'ID progetto, che avrà cifre diverse. In questa fase si verificano quattro eventi:
billing | utilizza l'account | |
project | crea | |
seleziona | scrive l'ID in | |
waits | esegue il polling finché il progetto non è effettivamente autorizzato a pubblicare, per un massimo di due minuti | |
👉🖥 Lo stesso progetto è ora nella console: il suo ID si trova nella scheda Informazioni sul progetto della dashboard:

5 · Installa e configura il lab (~4 min)
👉💻
cd ~/vibe-studio-multimodal
./setup_codelab.sh
Uno script, sicuro da eseguire di nuovo: una seconda esecuzione conserva tutte le risposte fornite la prima volta. All'inizio vengono poste due domande e con Invio viene accettato il valore predefinito per entrambe:
codice evento | la stanza di VibeTube in cui viene pubblicato il tuo video nella sessione 7. Digita il codice della stanza fornito dall'insegnante o premi Invio per | |
nome visualizzato | il nome con cui la stanza ti accredita o Invio per un nome creato dal tuo account. | |
Entrambi vengono inseriti in .env e possono essere modificati in un secondo momento, in .env o nella pagina di pubblicazione. Poi fa il resto da solo, in ordine:
uv env | installa uv se non è presente, quindi | |
apis | attiva sei API nel progetto selezionato nel passaggio 4 e conferma che ognuna risponda prima di continuare: | |
account | assicura che l'account di servizio Compute predefinito del progetto esista e disponga dei ruoli necessari per la build e il servizio della sessione 7 | |
.env | scrive | |
veo · room | conferma che | |
model | effettua una chiamata reale a Gemini, in modo che un progetto non valido non vada a buon fine in fase di configurazione e non all'interno di una sessione | |
starter | Inserisce i nove file che modificherai in posizione da | |
page | installa e crea | |
cloud | avvia la creazione del corpus RAG Engine e del Memory Bank in background, in modo che le sessioni 4 e 6 li trovino pronti | |
preflight | esegue | |
6 · Leggi il preflight (~1 min)
L'ultima cosa stampata nel passaggio 5 è il preflight: ogni riga è un probe reale, non un'ipotesi:
✓ 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) ha ragione: il grafico viene fornito vuoto e la sessione 3 è quella in cui lo compili. Anche le due righe - sono corrette: un corpus richiede un minuto o due per essere visualizzato e la banca circa venti secondi, e non avrai bisogno di nessuno dei due prima della sessione 4.
👉💻 Eseguilo di nuovo in qualsiasi momento per vedere a che punto sei: legge i tuoi file, il tuo progetto e il server in esecuzione, non un elenco di controllo:
cd ~/vibe-studio-multimodal
uv run python scripts/preflight.py
7 · Apri il workbench (~1 min)
Il workbench è già in esecuzione. Il passaggio 5 lo ha avviato in background, un processo uvicorn sulla porta 4600 che gestisce la pagina, ogni route /api/* e adk web montato su /workbench. Non c'è nulla da avviare e in questo lab non ti verrà mai chiesto di riavviare: una modifica viene applicata alla prossima esecuzione.
👉🖥 Fai clic sul link stampato dal preflight o in Cloud Shell: Anteprima web → Cambia porta → 4600. Chiamala scheda 2. Si apre su Sessione 1 · La storia.
I tre comandi che potresti voler utilizzare in un secondo momento, tutti da ~/vibe-studio-multimodal:
| arresta il workbench | |
| lo avvia in primo piano; Ctrl+C lo interrompe e interrompe ogni processo avviato | |
|
| |
Layout del repository
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 · La storia: la notte e il grafico che ne deriva
👉🖥 La scheda 2 si apre su Sessione 1 · La storia. Leggilo: sei attività disposte in cerchio attorno a una persona (ricerca la tendenza, esamina l'arretrato, scegli una direzione, controlla le norme, scrivi lo script, esegui il rendering del clip) e il cerchio si ripete ogni notte. Gemini può fare la ricerca, il backlog e lo script da solo; Veo può eseguire il rendering da solo; la scelta e il controllo spettano alla persona.
Sotto l'anello si trova il grafico che verrà creato nel resto del lab, con tredici nodi che vanno da un'idea a un clip e al relativo poster. Passa il mouse sopra un nodo per vedere cosa fa e la sessione che lo crea. Non c'è nulla da modificare qui; il pulsante Genera il primo poster della pagina è la sessione 2.
Il vocabolario, quindi le sei pagine successive vengono lette senza interruzioni:
parola | cosa significa in questo contesto |
agent | un ADK |
node | un passaggio del grafico: una funzione Python, un agente, un join, un router |
edge | una tupla di nodi che vengono eseguiti in ordine; il grafo è un elenco di questi nodi. Sessione 3 |
session | il record di un'esecuzione in |
stato | un dizionario condiviso dai nodi in base al nome della chiave; un nodo scrive |
memoria | ciò che sopravvive a una sessione: Memory Bank, tramite callback. Sessione 6 |
4. 2 · Generazione di immagini: un agente e il modello di immagine come strumento

👉🖥 Nel workbench, apri Sessione 2 · Generazione di immagini. Prima del grafico, la cosa più semplice che potrebbe funzionare: un Agent la cui istruzione è l'intera notte in prosa, tre funzioni Python come mani e un'esecuzione che termina con un'immagine.
Un agente è un modello, un'istruzione e degli strumenti
stage0_prompt/agent.py definisce root_agent con tre dei campi di un agente ADK. model è il modello Gemini di agent/platform/config.py. instruction è il prompt di sistema: sei attività in un colpo solo, l'ultima delle quali è un'immagine. tools contiene tre funzioni.
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."))
Un modello è un mondo chiuso: ragiona sui suoi pesi e sui token nel suo contesto. Uno strumento è una funzione Python che il modello può richiedere. L'ADK legge la firma e la docstring di ogni funzione e le dichiara al modello. Quando il modello ne richiede una, genera un evento function_call, il runtime esegue il codice Python e scrive un evento function_response nel contesto, quindi il modello continua con il risultato ottenuto.
Tre funzioni, una delle quali disegna
Due su tre leggono la ricerca. Il terzo chiede al modello di immagine un poster:
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 chiama agent/platform/images.py, il modello di immagine racchiuso in una funzione semplice come videogen.py racchiude Veo: un prefisso in stile house (il look accogliente di Halloween low-poly del canale, arancione zucca e crema a lume di candela su prugna di mezzanotte), il soggetto, un frame verticale 9:16, un PNG sotto runs/media/posters, pubblicato su /static/posters. Con STUDIO_REAL_IMAGES=0, copia un poster sostitutivo e contrassegna il risultato prebaked, in modo che il lab venga eseguito ovunque.
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."""
La stessa chiamata è un nodo nel grafico della sessione 5.
Pratica: la modifica
Come spedito, make_poster restituisce un dict che indica che il poster è stato saltato. 👉🖥 Nell'editor di Workbench, fai in modo che chiami il modello di immagine, al posto della riga TODO:
from agent.platform import images
return images.generate(subject, name="solo")
risparmia, Le righe di verifica leggono il file: tre strumenti sull'agente, la chiamata al modello di immagine in atto. 👉🖥 Fai clic su Controlla i carichi del workflow, poi su Apri ADK Web e invia:
tonight's idea: a tiny ghost learning to haunt a sock drawer
Cosa aspettarsi e perché
- Due eventi dello strumento prima della risposta, poi una domanda.
check_trendseread_backlogvengono eseguiti perché l'istruzione diceva di controllare e guardare, ma il modello non aveva nessuna delle due azioni nei suoi pesi. La risposta propone una direzione e ti chiede di confermare, perché l'istruzione richiedeva prima l'accordo. - Il secondo messaggio riceve tutto. Invia
skip the questions, describe the video and draw the poster. Il modello descrive il video, chiamamake_posteruna volta con un oggetto scritto da sé e ti dice dove è stato salvato il file PNG. Il riquadro di verifica mostra il poster accanto alle righe: una scena di Halloween nello stile del canale, disegnata in circa otto secondi. - Perché non è un canale. La ricerca è integrata in una prosa che non può essere citata. La policy è stata certificata dal modello che ha scritto la direzione e il poster è stato disegnato prima che venisse controllato. L'accordo è stato discusso in un messaggio. Il testo del prompt non è un gate. Il resto del lab suddivide questi sei job in un grafico disegnato.
La pagina si conclude con Composizione di un agente ADK, un riferimento a cui puoi tornare: il modello, il contesto con cui ragiona, gli strumenti e i collaboratori attraverso cui agisce, i callback che racchiudono ogni chiamata e la sessione e la memoria che vivono al di fuori di esso.
5. 3 · Workflows — the fan-out and the join, the router and the loop

👉🖥 Nel workbench, apri Sessione 3 · Workflows, parti 3a e 3b. Ogni parte è creata in un'app sandbox, in modo da poterla eseguire in adk web in modo indipendente: il fan-out in stage1_fanout, il router e il relativo loop in stage3_router.
Fan-out e join (3a)
Un ADK Workflow è un elenco di archi. Ogni voce è una catena, una tupla di nodi che vengono eseguiti in ordine. Due catene che partono da START vengono eseguite in parallelo. Una catena che termina con un JoinNode genera un report per questo nodo e l'unione attende ogni catena che termina lì prima di continuare, passando un dizionario con chiave in base al nome del nodo. Un target scritto come dizionario mappa i nomi delle route di un router ai nodi. La pagina 3a del quaderno di esercizi mostra le forme e contiene un esercizio di scrittura dei bordi.
Tipo di nodo | Scritto come | Descrizione |
Nodo funzione |
| Python semplice. Può anche scrivere lo stato condiviso con |
Nodo di join |
| Attende che ogni catena in entrata abbia generato un report, quindi trasmette tutti i relativi output come un unico dizionario. |
Nodo agente |
| Una chiamata al modello in modalità |
Router |
| Un nodo di funzione il cui ritorno assegna un nome all'arco in uscita. |
Nodo di intervento umano |
| Sospende il grafico finché una persona non risponde a un modulo. Sessione 5. |
Il primo grafico è la ricerca della sessione 2 come nodi. scan_trends e read_backlog in agent/graph.py sono i due lettori; read_backlog scrive anche l'idea di stasera nello stato condiviso. stage1_fanout/agent.py definisce il join e crea il relativo Workflow da un elenco denominato EDGES, che viene fornito vuoto (0 edges del preflight). 👉🖥 Compilalo con le due catene:
EDGES = [(START, scan_trends, join_research),
(START, read_backlog, join_research)]
👉🖥 Eseguilo in adk web. Due nodi si accendono insieme, poi si uniscono; apri l'evento di join_research e leggi un dizionario con una chiave scan_trends e una chiave read_backlog. Nessuna chiamata al modello. Niente riformatta il dizionario del join: il nodo successivo, in 3b, è un agente e un nodo agente riceve il suo node_input come messaggio, quindi un dizionario arriva come JSON che il modello legge direttamente.
Il router e il loop (3b)
Un Agent all'interno di un Workflow viene eseguito in modalità single_turn: riceve l'output del nodo precedente come messaggio, effettua una chiamata al modello e risponde una volta. Con output_schema, la risposta viene convalidata rispetto a un modello Pydantic prima che il nodo successivo la veda, quindi il grafico passa oggetti tipizzati, non prosa. propose_directions legge il dizionario di ricerca della congiunzione e propone quattro candidati, ognuno dei quali è una scena di Halloween. Il candidato 4 è l'esca: il suo titolo contiene una parola che il canale rifiuta, quindi il router ha qualcosa da intercettare. Ogni candidato porta con sé un poster_idea, una riga che indica la singola immagine che vende il video; lo scripter lo conserva e il media desk lo disegna nella sessione 5.
Un router è un nodo di funzione il cui valore restituito indica l'edge che l'esecuzione segue. Nessuna chiamata al modello: policy_check legge agent/policy_words.txt al momento della decisione, confronta le parole intere con il titolo e l'angolazione della direzione e le indirizza prima che venga scritto uno script o che vengano spesi soldi. La policy è costituita da dati: modifica il file e la prossima esecuzione lo leggerà.
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 è il fan-out più propose_directions e choose_direction, un sostituto della porta umana che crei nella sessione 5: sceglie il candidato il cui numero termina l'idea di stasera (..., pick 4), altrimenti il primo, e scrive la scelta nello stato. scripter è un agente single_turn che trasforma una direzione approvata in un Script: un titolo, una descrizione, dei tag, una battuta iniziale, tre inquadrature e un'idea per un poster, ogni inquadratura un'immagine di Halloween con l'aspetto del canale.
Il loop dietro l'uscita rifiutata. quarantine viene fornito come agente in modalità attività. Un Agent ha un mode: chat è il loop di conversazione che un agente root esegue con una persona, single_turn è una chiamata con una risposta strutturata e task è un loop di chiamate di strumenti che l'agente esegue autonomamente fino a quando non chiama l'finish_task integrato, i cui argomenti ADK derivano da output_schema. La quarantena chiama find_policy_hits nella direzione rifiutata, chiede a suggest_replacement il sostituto approvato del canale per ogni parola (agent/policy_replacements.txt, di nuovo i dati), riscrive, controlla di nuovo e termina con un CleanedDirection digitato. Leggilo sulla pagina, non digitarlo.
quarantine = Agent(
name="quarantine",
model=config.MODEL,
mode="task",
instruction=QUARANTINE_INSTRUCTION,
tools=[find_policy_hits, suggest_replacement],
output_schema=CleanedDirection)
Come spedita, la catena va dal plettro direttamente allo scripter. 👉🖥 Posiziona il router tra di loro, con le sue due uscite e il percorso di ritorno dalla quarantena; tre righe sostituiscono una:
(join_research, propose_directions, choose_direction, policy_check),
(policy_check, {"OK": scripter, "BLOCK": quarantine}),
(quarantine, scripter),
👉🖥 Esegui entrambe le route. Invia l'idea così com'è: choose_direction prende il candidato 1, policy_check emette OK e lo scripter scrive uno script strutturato. Fai clic su Nuova sessione e invia di nuovo l'idea con , pick 4 alla fine: il candidato 4 è l'esca, policy_check emette BLOCK, la quarantena chiama i suoi strumenti, riscrive le parole rifiutate, chiama finish_task e la direzione ripulita torna allo scripter. Le righe di verifica leggono entrambe le esecuzioni dall'archivio della sessione.
Ogni frase del prompt della sessione 2 ora ha un nodo: la ricerca è composta da due lettori e un join, il rifiuto è un router e un agente di attività, lo script è un nodo agente. Nell'intero grafico, la scelta sostitutiva è costituita da due nodi di funzione, direction_gate e persist_direction, che vedrai eseguire direttamente fino a quando la sessione 5 non interrompe la porta. L'accordo con il creator è ancora in fase di definizione, così come la ricerca, che non ha ancora coinvolto il pubblico.
6. 4 · Ingestione multimodale: ciò che il pubblico ha inviato, letto in un unico corpus

👉🖥 Nel workbench, apri Sessione 4 · Importazione multimodale, parti 4a e 4b. Il pubblico non si è limitato a scrivere. Un lettore per tipo trasforma tutto ciò che ha inviato in testo con la stessa forma, un corpus di RAG Engine contiene tutto e un terzo lettore nel fan-out esegue la ricerca per significato e restituisce i contenuti multimediali.
Contenuti inviati dal pubblico (4a)
agent/comments.md contiene dodici commenti scritti sotto i sei video di Halloween del canale. agent/audience/ contiene il resto: otto note vocali (voice/*.mp3), quattordici foto delle stanze degli spettatori (photo/*.jpg) e sei clip (clip/*.mp4), una panoramica lenta su una foto con la voce fuori campo dello spettatore. La pagina 4a si apre su tutto: ogni nota vocale viene riprodotta, ogni foto viene mostrata, ogni clip viene eseguito.

RAG Engine recupera il testo. Una nota vocale, una foto o un clip possono essere aggiunti al corpus solo come i poster precedenti in rag.caption_posters: un modello li trasforma in una riga di testo che nomina il file, la riga diventa un passaggio e, quando il passaggio viene restituito, il file viene restituito con questo. agent/platform/audience.py ha un lettore per tipo. Ognuno invia un file e un prompt a Gemini e scrive una riga per file:
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 fa lo stesso con image/jpeg e PHOTO_PROMPT; watch_clips con video/mp4 e CLIP_PROMPT, e Gemini guarda l'immagine e ascolta la colonna sonora in un'unica chiamata. Ogni lettore scrive la stessa forma di riga, quindi il chunking, l'embedding e il recupero non sanno mai da quale tipo di passaggio proviene:
- 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 ...

Il corpus. Vertex AI RAG Engine è il recupero di documenti: un corpus viene suddiviso in blocchi di circa 120 token, ogni passaggio viene incorporato con text-embedding-005 e una query viene incorporata nello stesso modo e riceve una risposta con i passaggi più vicini per significato, non per parole condivise. "cozy little hauntings at home" trova il commento che chiede un po' di magia in piccole stanze, la nota vocale sul pipistrello che abbraccia la lampadina e il clip la cui colonna sonora dice che il fantasma è un bicchiere di carta.
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)))
La configurazione ha già iniziato a creare il corpus; la riga di connessione della pagina 4a indica se è attivo e lo crea se la configurazione non è riuscita. Dai pulsanti della pagina o dal terminale: python -m agent.platform.rag crea il corpus e lo registra in runs/ragcorpus.json; python -m agent.platform.rag load --audience esegue ogni lettore in audience.READERS, quindi carica ciò che hanno scritto accanto ai commenti e alle didascalie dei poster, ogni file con il proprio nome, e attende la risposta dell'indice; python -m agent.platform.rag query "cozy little hauntings at home" mostra il recupero dei passaggi che non condividono alcuna parola con la query.
La modifica. Al momento della spedizione, READERS contiene solo read_comments; i tre lettori multimediali si trovano dietro un # . 👉🖥 Esegui l'importazione una volta così com'è: l'output indica not read (no reader wired in audience.READERS): voice, photo, clip e vengono inseriti solo i commenti e i sottotitoli codificati. Quindi elimina # :
transcribe_voice, caption_photos, watch_clips,
👉🖥 Salva ed esegui di nuovo l'importazione. I lettori impiegano circa tre minuti per i quaranta elementi; la console dice heard, saw e watched una riga alla volta e ogni scheda della griglia si riempie con la sua riga. Le righe di verifica leggono il file, l'output dei lettori e il corpus: quattro lettori collegati, otto note vocali ascoltate, quattordici foto viste, sei clip guardati, cinque file nel corpus.
Il terzo lettore (4b)
Il feedback del pubblico è una ricerca, condivisa da ogni esecuzione, quindi entra nel grafico dove si trova la ricerca: come terzo lettore nel join. read_feedback in agent/graph.py prende l'idea di stasera, chiede al corpus i passaggi più vicini e li segnala a join_research, che ne aspetta tre. Il bordo (START, read_feedback, join_research) viene spedito in stage5_rag/agent.py, la forma insegnata nella sessione 3; la modifica è all'interno del nodo. Recupero, non una chiamata al modello, e deterministico: la stessa idea trova gli stessi passaggi mentre il proponente varia.
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
Ogni riga del corpus proveniente da una nota vocale, una foto, un clip o un poster indica il nome del file. rag.media_in(hits) legge questi nomi dai passaggi e li trasforma in URL che la pagina può mostrare e riprodurre. 👉🖥 Restituisci i contenuti multimediali insieme ai passaggi, in modo che i contenuti inviati dal pubblico viaggino insieme a ciò che ha detto:
return Event(output={"query": query, "feedback": [h["text"] for h in hits],
"media": rag.media_in(hits)})
👉🖥 Esegui stage5_rag con un'idea. Apri read_feedback: la query è la tua idea, l'output contiene i cinque passaggi più vicini e un elenco media con un URL per file. Apri join_research: tre chiavi. Apri propose_directions: i candidati da 1 a 3 citano feedback nelle loro prove. Il riquadro di verifica mostra ciò che il lettore ha esaminato sotto le righe: un clip, tre note vocali e un poster per il fantasma nella luce del frigorifero.

7. 5 · Generazione di video: due tipi di sospensione, un unico meccanismo

👉🖥 Nel workbench, apri Sessione 5 · Generazione di video, parti 5a e 5b, create in stage6_video, l'intero grafico. Il clip richiede alcuni minuti e il poster alcuni secondi, mentre l'esecuzione si interrompe due volte: una per te e una per Veo. I due stop sono un unico meccanismo e i due rami del desk condividono ciò che hanno realizzato in base al nome.
Due tipi di sospensione (5a)
Nessun messaggio resta lì. Un'esecuzione che deve essere interrotta scrive una chiamata aperta nella sua sessione e termina; chiunque risponda a quella chiamata, una persona o un processo, la continua. La pagina si apre su una tabella:
per una persona · | per uno strumento lento · | |
chi interrompe la corsa |
|
|
cosa aspetta | una persona: una di quattro carte | un lavoro lento: l'operazione di Veo, minuti |
ciò che si trova nella sessione | una chiamata | una chiamata |
come riprende | un | un |
chi lo invia |
|
|
dove vive nel frattempo |
| stessa riga, stesso ID · non viene eseguito nulla |

Per una persona. In un singolo prompt, "chiedi al creator di confermare" è un consiglio e la sessione 2 lo ha ignorato in un messaggio. In un workflow, la pausa è del motore. Un nodo che genera RequestInput sospende l'esecuzione: ADK scrive una chiamata aperta denominata adk_request_input nella sessione con message, response_schema e payload, le assegna un interrupt_id e torna al chiamante. Nessuna attesa di thread. L'unica cosa che lo riprende è un function_response che contiene l'ID, convalidato rispetto allo schema; un messaggio chat al workflow è un nuovo turno, non una risposta. Lo schema è anche il modulo: adk web ne esegue il rendering e l'app VibeStudio esegue il rendering dello stesso schema sotto forma di quattro schede. direction_gate scrive già i candidati nello stato. 👉🖥 Per interrompere la rotazione:
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})
La tua risposta arriva a persist_direction come node_input, un dizionario con una chiave, pick. I quattro candidati arrivano in un altro modo: il gate li ha scritti nello stato condiviso in candidates e l'ADK associa una chiave di stato a un parametro con lo stesso nome. Il nodo risolve la scelta in un candidato e scrive direction, angle, hook e user:prefs con Event(state=...). Le chiavi con prefisso user: vengono mantenute tra le sessioni per quell'utente.
Per uno strumento lento. Il rendering di Veo richiede alcuni minuti e un grafico non dovrebbe attendere così tanto. render_submit in agent/desk.py avvia il job e restituisce immediatamente una ricevuta con lo stato 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"]}
Racchiusa in LongRunningFunctionTool, questa ricevuta ha un significato per l'ADK: il turno dell'agente termina, il workflow viene sospeso in render_desk e la chiamata aperta con il relativo ID e la ricevuta è una riga in runs/sessions.db. 👉🖥 In stage6_video/agent.py, aggrega lo strumento:
tools=[LongRunningFunctionTool(render_submit)])
Riprendi per ID, da un altro processo. agent/deliver.py trova la chiamata in attesa nello spazio di archiviazione della sessione, esegue il polling di Veo finché il clip non esiste, scrive il risultato in runs/state.json e invia alla stessa sessione un function_response con l'ID della chiamata. 👉🖥 Crea la parte della risposta:
part = Part(function_response=FunctionResponse(
id=row["call_id"], name=row["name"], response=response))
👉🖥 Esegui stage6_video finché non si arresta due volte. Invia un'idea: l'esecuzione si interrompe su un modulo creato in base allo schema, con i quattro candidati nel payload. Digita 2 in pick e invia; il grafico riprende in persist_direction. Prova il test di interruzione qui: sospendi il modulo, arresta il server (scripts/stop.sh nella scheda 1), riavvialo (scripts/restart.sh), riapri la sessione. La forma è ancora in piedi, perché una pausa è una riga, non un filo. Dopo script_ready, render_desk chiama render_submit, riceve la conferma, risponde WAITING e l'esecuzione termina; la sessione mantiene l'ID chiamata. Esegui la pubblicazione dalla console della pagina o dalla scheda 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
👉🖥 Fai clic su Aggiorna adk web: function_response segue la chiamata in attesa, join_media rilascia e store_release viene eseguito. Otto righe di verifica leggono i due arresti della sessione: la chiamata del gate e la tua risposta, la ricezione del rendering e la sua consegna.
Stato e scrivania (5b)
Il desk multimediale ha di nuovo la struttura di 3a, un fan-out e un join, con un ramo lento. script_ready invia lo script in due modi. render_desk invia il clip e l'esecuzione viene sospesa. make_poster è lo strumento della sessione 2 come nodo: il poster_idea dello script, lo stile del canale e ciò che Memory Bank sa dei gusti del creator in fatto di poster (vincolato dallo stato come memory_facts, sessione 6) vanno al modello di immagine e un PNG 9:16 torna indietro in pochi secondi. join_media attende l'operazione lenta e store_release scrive ciò che ha trovato nello stato.
Condividi per nome. store_release non riceve mai il poster. Dichiara un parametro denominato poster_url e l'ADK lo associa dallo stato condiviso, perché un nodo nell'altro ramo ha scritto quella chiave. Nessun payload passa da un ramo all'altro:
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'}")
Come spedito, make_poster restituisce il poster come output e un messaggio, mentre store_release dice poster: not in state. 👉🖥 Aggiungi la scrittura dello stato al suo yield finale:
state={"poster_url": rec.get("url", ""), "poster_path": rec.get("path", "")},
message=f"poster: {said}")
👉🖥 Esegui un'altra volta e consegna. Evento di apertura di make_poster: l'URL del poster nel suo output e un delta di stato con poster_url. Apri store_release: il messaggio indica l'autore del post. Il blocco Sessione di questa esecuzione della pagina mostra la stessa riga della scheda Stato di adk web: l'ID sessione necessario per un ripristino, la chiamata aperta e le chiavi scritte dai nodi, tra cui poster_url. Il riquadro di verifica mostra il clip e la locandina affiancati.

8. 6 · Memoria: ciò che il canale ricorda tra una notte e l'altra

👉🖥 Nel workbench, apri Sessione 6 · Memoria, parti 6a e 6b, create in stage4_memory. La sessione 5 riguardava lo stato di un'esecuzione. Questa sessione riguarda ciò che il canale ricorda tra un'esecuzione e l'altra: ciò che il grafico sente dall'esterno, tramite due callback.
Memory Bank (6a)
Vertex AI Memory Bank è una memoria a lungo termine gestita su una persona. I fatti sono contenuti in un ambito, il nome dell'app e l'ID utente, e negli argomenti definiti da te. Quando gli fornisci uno scambio con memories.generate, estrae ciò che si adatta a un argomento, lo incorpora e lo consolida con ciò che già possiede: viene creato un nuovo fatto, uno modificato viene aggiornato, una ripetizione viene unita. memories.retrieve restituisce i fatti per un ambito. Entrambe le chiamate sono attive in 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.",
}
Il terzo argomento è il gusto del canale in fatto di immagini. La release ha scritto nella sessione 5: un poster spedito viene descritto in una frase e memorizzato, mentre il poster successivo viene disegnato con quella frase nel prompt.
La configurazione ha già iniziato a creare il banco; la riga di connessione della pagina 6a indica se è attivo. Dai pulsanti della pagina o dal terminale: python -m agent.platform.bank crea la banca e i relativi argomenti e memorizza nella cache il nome della risorsa in runs/memorybank.json; python -m agent.platform.bank load genera quattro sessioni passate (due ere di animali con una regola di stile, un'era di gadget, un'era fantasy recente); python -m agent.platform.bank list mostra cosa sono diventate le trascrizioni: dichiarazioni di fatto consolidate, non trascrizioni.
Callback (6b)
La memoria non è un nodo. Un callback è una funzione che passi a un Agent; l'ADK la chiama in un momento fisso con il contesto live e la restituzione di None consente di eseguire l'azione normale. Tre coppie: intorno al turno dell'agente (before_agent_callback, after_agent_callback, con un CallbackContext), intorno a ogni chiamata del modello (before_model_callback con LlmRequest, after_model_callback con LlmResponse), intorno a ogni chiamata dello strumento. Guardrail, telemetria, cache e inserimento del contesto vanno qui, senza un nodo nel grafico.
In stage4_memory/agent.py, propose_directions legge la banca prima della chiamata al modello: recall_taste recupera i fatti, li aggiunge alla richiesta come blocco MEMORY, dal più vecchio al più recente, e indica al modello di orientare i candidati da 1 a 3 verso il gusto attuale e di trattare le regole come vincoli. Inoltre, scrive i fatti da dichiarare come memory_facts, dove make_poster li trova. 👉🖥 Collegalo:
output_schema=Directions,
before_model_callback=recall_taste)
Lo scripter scrive ciò che il creator ha scelto dopo il suo turno: remember_pick legge direction dallo stato, scrive una frase sulla scelta e la consegna a memories.generate. 👉🖥 Collegalo:
output_schema=Script,
after_agent_callback=remember_pick)
👉🖥 Esegui con un messaggio vuoto e scegli una carta. Apri l'evento propose_directions: la richiesta contiene le tre epoche e i candidati da 1 a 3 tendono verso il gusto attuale, mentre le tendenze suggeriscono qualcos'altro. Dopo il suggeritore, fai clic su Mostra la banca dati: un ricordo è stato modificato o aggiunto per la scelta di stasera. Esegui una seconda notte: ricorda la prima.
9. 7 · Deployment e pubblicazione: il grafico all'interno di un'app, la notte su VibeTube

👉🖥 Nel workbench, apri Sessione 7 · Esegui il deployment e pubblica, parti 7a e 7b.
Esegui il deployment di VibeStudio (7a)
Nelle sandbox, adk web ha guidato il grafico. VibeStudio lo gestisce con Runner di ADK, su un thread di lavoro con il proprio loop di eventi, e raggruppa ogni evento in uno stato di esecuzione che la pagina legge tramite Server-Sent Events: i lettori, la tua scelta di quattro carte, il poster quando arriva, il rendering quando viene consegnato. La risposta del gate e la consegna del rendering sono la stessa chiamata, una parte function_response, e l'app esegue il polling di Veo anziché attendere la console.
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/ è una copia byte per byte del agent/ del lab, controllata da checks/verify_app.py. L'elenco completo degli archi in agent/graph.py contiene tutte le strutture di questo codelab in un unico posto, tredici nodi; viene fornito già compilato, per essere letto, non digitato:
(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),
👉🖥 Esegui il deployment con il pulsante Esegui il deployment del workbench o 👉💻 dalla scheda 1:
cd ~/vibe-studio-multimodal
uv run python vibestudio/deploy.py
che viene eseguito con i valori della tua .env compilati:
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 crea il container con Cloud Build ed esegue il deployment in un unico passaggio; la prima build richiede quattro o cinque minuti. --session-affinity con un'istanza calda mantiene un'esecuzione sospesa sull'istanza che la contiene: l'archivio della sessione 5, ancora una volta. In un deployment più grande, l'archivio delle sessioni viene spostato in VertexAiSessionService e l'affinità scompare. La build e il servizio vengono eseguiti come service account Compute predefinito del progetto, a cui la configurazione ha concesso i ruoli necessari.
👉🖥 Apri l'URL del servizio ed esegui una notte nell'app: un'idea, una scelta, attendi il clip e il poster. La scheda Cronologia conserva entrambe le versioni.
Pubblica (7b)
La pubblicazione è una richiesta POST multiparte dall'app all'endpoint della stanza su VibeTube, in vibestudio/server/platform/publish.py. Il clip è il video, il poster è la miniatura e il profilo indica il nome della stanza e il tuo:
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 conserva un video per projectId e stanza e il progetto cloud inviato dall'app è il tuo progetto Google Cloud, quindi la pubblicazione sostituisce il video anziché aggiungerne uno. Non viene mai caricato un poster sostitutivo; quello reale è la miniatura, byte per byte.
L'ultima modifica è una configurazione, non una riga di codice. 👉🖥 Nella pagina 7b: l'URL di VibeStudio (compilato dal deployment di 7a), il codice evento della stanza, esattamente come lo scrive VibeTube (quello che hai fornito durante la configurazione, a meno che l'insegnante non ne abbia aperto un altro) e il tuo nome. Premi Pubblica. Il workbench trasmette il clic all'app, l'app pubblica la serata e le righe sottostanti leggono il risultato dall'app e da VibeTube: l'app risponde, la serata è terminata con un poster reale, il nome in codice indica una stanza aperta, VibeTube ha restituito un ID video, il video è sulla bacheca e la sua miniatura è il tuo poster. La scheda dei risultati mostra la miniatura su VibeTube accanto al poster nell'app.

10. Cosa hai creato
👉🖥 La parte finale della pagina 7b mostra l'intero grafico, tredici nodi da un'idea a un clip e al relativo poster; passa il mouse sopra un nodo per vedere cosa fa e la sessione che lo ha creato.
Sessione | Cosa ha insegnato | Scritto come |
2 · Generazione di immagini | Un agente: modello, istruzioni, strumenti; il modello di immagine come strumento; cosa non può promettere un prompt |
|
3 · Workflows | L'elenco di archi, il fan-out e il join, un router con un loop in modalità attività dietro |
|
4 · Importazione multimodale | Un lettore per tipo, una forma di linea, un corpus; un terzo lettore per significato, con i media |
|
5 · Generazione di video | Due tipi di sospensione, un meccanismo; l'archivio delle sessioni; il poster condiviso per nome |
|
6 · Memoria | Memoria tra le notti, due callback |
|
7 · Esegui il deployment e pubblica | Il grafico all'interno di un'app su Cloud Run; la notte su VibeTube |
|
Cinque regole seguite dal grafico:
- Sospendi invece di attendere. Una pausa è una riga nell'archivio delle sessioni, non un thread.
- Riprendi tutto nello stesso modo: in base all'ID della chiamata aperta, indipendentemente dal fatto che risponda una persona o un processo.
- Condividi i dati per nome nello stato, anziché inserirli in ogni payload.
- Decidi con il codice prima di pagare per un modello. La policy è un file di testo e un router e viene eseguita prima del rendering.
- Leggi tutto ciò che non è testo in testo prima che raggiunga il corpus, con il nome del file all'interno della riga in modo che il file possa tornare.
Un prompt non può promettere ordine, un arresto o un controllo. Un grafico sì.
Passaggi successivi
VertexAiSessionServiceper le sessioni che sopravvivono a un'istanza e abbandona--session-affinity.- Un webhook che risponde alla chiamata del rendering anziché a un poller: stesso
FunctionResponse(id=...), mittente diverso. - Un secondo
RequestInputprima della pubblicazione: lo schema è il modulo, quindi è un nodo e non un codice di pagina. - I nuovi commenti, le note vocali e i clip del pubblico aggiunti al corpus dopo ogni uscita, insieme al poster sotto cui sono stati inviati.
Esegui la pulizia
Tutto ciò che è stato creato in questo lab si trova nel progetto creato nel passaggio 4, quindi la pulizia più economica consiste nell'eliminare il progetto. In Cloud Shell:
gcloud projects delete $(cat ~/project_id.txt)
Per mantenere il progetto e rimuovere solo le parti a pagamento: il servizio Cloud Run (gcloud run services delete vibestudio --region us-central1), il corpus RAG e il Memory Bank (i nomi delle risorse sono in runs/ragcorpus.json e runs/memorybank.json, mentre le pagine Agent Engine e RAG Engine della console li eliminano) e l'immagine container in Artifact Registry.