Inspiration

The video was working perfectly and I couldn't hear a word.

She's in a kitchen. Grey sweatshirt with a coffee stain on it, freckles, hair tied back badly. She leans toward the phone, gets to the number, and stops — that small involuntary pause of someone who still can't believe it. Behind her, takeaway menus on the fridge. Everything in the frame is right.

There is no sound. Under her runs a music bed, correctly mixed, correctly ducked, at exactly the level the specification asks for. I measured it instead of arguing with myself about it: \( -42 \) dBFS. The renderer was muting every clip, on one line, with a comment explaining why — an assumption that had been true for a year and had quietly stopped being true.

Two months earlier I had killed a project called Casanova, an agent that would run your dating profiles for you. A stranger found the waiting list and took it apart in the comments. The research he pushed me into found the real problem, which was not his argument at all: liveness checks. Film yourself touching your nose, right now. An agent that hits one has to wake up a human and ask them to open an unfamiliar session on the account it could get banned from. Trust dies at that exact moment.

That taught me the rule this hackathon is named after. An agent for humans should do the repetitive work in the background and only come to a person when there is a real decision to make — never to hand them a chore.

Short-form video for a small app is almost entirely chore. An app pays a creator €2,000 for one video. The video gets 4,000 views. Nobody can say whether the next one will do ten times better or worse, and the only way to find out is to pay again. The economics only close at volume, and volume needs people — writers, performers, editors, somebody to post at 18:30 every day for six months. So almost nobody does it.

I wanted to know whether that whole loop could run with nobody in it except the founder, at the moments that actually need them. Not a video generator. A creator that wakes up.

Why me

I should explain why a sixteen-year-old is building a marketing product, because the answer is also why it looks the way it does.

When GPT-3 came out I had just finished learning HTML and CSS. I concluded the race was over and stopped learning to program. I went and learned marketing, persuasion and storytelling instead — five months on a treatise about hooks: Campbell and Vogler, Syd Field's plot points, the Pixar spine, why a story holds attention when a fact doesn't. And one person who can write a hook is still one person. You cannot be ten creators posting at 07:00 on a Tuesday from ten different faces.

Earlier this year I wanted an internship at Google, so I walked to their Paris office. I wrote the cover letter on my phone on the train and arrived twenty minutes after the deadline; the guard let me in anyway. Two days, no answer, so I went back — and someone came out, heard the whole story, and took me inside. The internship turned out to be closed to me, and I came out with a fact I now trust: the second visit is a different visit.

That sentence is the product. An agent that posts once and stops has learned nothing. An agent that comes back tomorrow with what yesterday taught it is a different agent.

What it does

OttoUGC is a Professional Agent for founders and small app teams: ten synthetic creators that run on the brand's own YouTube channels. Each one has a face, a room, a way of speaking, a strategy and a memory.

The human does three things, and only three.

  1. Paste a link. The agent crawls the site — home, pricing, about, FAQ — and hands back a short brief about the company, every claim tied to the sentence it came from. The founder corrects six sentences instead of filling a thirty-field form.
  2. Decide whether to go live. Publishing is a switch the founder throws. Until then everything runs in dry run.
  3. Read the verdicts. Every keep, double-down or drop names the number that caused it.

Everything else runs in the background. At 07:00 each creator agent wakes up on its own — Cloud Scheduler, not a person — and works through its day. It reads what it learned yesterday. It looks at what its last videos did at 2h, 24h and 72h. It picks a shape it has not just used, because two videos with the same length, speech mode and on-screen text look identical to somebody scrolling. It writes a situation rather than an advert, and the product arrives once, late, in passing. It draws the whole thing as a storyboard, films it, cuts it, checks it, schedules it. Then it writes down what it learned, with the numbers in it.

Every post is scored on rates, never absolutes, so a bigger channel doesn't always win and the fleet keeps learning about the creative:

$$S = 1.8\,W_{3s} + 4.5\,\mathrm{CR}_{100} + 5.0\,\frac{\text{shares}}{\text{views}} + 3.5\,\frac{\text{saves}}{\text{views}}$$

Below threshold three times, a format is dropped. Above it, doubled. The channel's posterior over formats updates, and tomorrow's choice is drawn from it by Thompson sampling — mostly exploiting what works, deliberately exploring what hasn't been tried.

There is a hard stop, and it is not in the prompt. No number reaches a video unless it appears in the brand's own published material. Absolute promises and competitor names are blocked the same way.

How we built it

Every agent is a Strands Agent. The manager, each creator, QA, the analyst and the weekly review run on the Strands Agents SDK (TypeScript, @strands-agents/sdk).

  • The loop. Each run builds a Strands Agent with the role's system prompt, its subset of the 53 tools (knowledge, memory, strategy, generation, editing, publishing, analytics) as FunctionTools, and a turn limit.
  • The model. Gemini 3.7 Flash, through Strands' GoogleModel on the existing Vertex AI client.
  • The trace. Strands hooks (AfterModelCallEvent) and the tool callbacks write every message, tool call, argument, result and duration to an append-only agent_steps table. That is what the dashboard renders, and what makes "why did channel 4 post that?" answerable three weeks later.
  • Errors go back to the model. A tool that throws becomes an error result the model can recover from. The one exception is money: a tool that would push the day past its generation budget cancels the agent, and the run fails loudly.
  • The single-shot writers are Strands too. The company brief, the audience profile, the character sheet and the scene are tool-less Strands agents with a JSON response schema.

The media. Six Google models on Vertex AI, each doing one job:

  • Gemini 3.7 Flash reasons for the agents and judges every storyboard frame before it is animated.
  • Nano Banana 2 (Gemini 3 Pro Image) builds a seven-photograph character sheet, then one storyboard panel per shot.
  • Gemini Omni Flash animates an approved panel with the dialogue in sync inside the clip.
  • Lyria 002 scores it, and Chirp 3 HD narrates where the scenario calls for it.
  • Speech-to-Text reads the word timings back off the generated audio for the captions.

Editing and infrastructure.

  • Editing is a typed timeline the agent edits with tools, rendered by Remotion at 1080×1920 and 60fps.
  • The stack is Next.js on Cloud Run, with Cloud SQL (Postgres, Drizzle), Cloud Scheduler and Cloud Storage.

The craft is deliberately not the model's job. A model asked to write a video prompt writes adjective salad — cinematic, beautiful, 8k — and that produces exactly the plastic look that gets a UGC account dismissed in half a second. So shots are compiled from closed, typed vocabularies: framing, motivated light, sensor behaviour, performance direction. The model supplies only what genuinely requires judgement — who is in the shot, what happens, what is said. The agent chooses which video exists today and why.

The storyboard is the one idea I would keep if I had to throw the rest away. The image model is far better than the video model at composition and at holding a face. So the frame is decided in stills: a panel per shot, each conditioned on the character's photographs and on the panel before it, so the room, the clothes and the light carry over as pixels rather than adjectives. A vision pass rejects any panel with the wrong face, a fabricated interface or burnt-in text. Only approved panels are animated.

I wrote all of it with AI coding assistants, one to two hours a day, mostly at night. What I contributed was the precision of what I asked for and the shape of the system: what runs on a timer, what is allowed to fail, and what is not.

Challenges we ran into

The agent could not exist, and it was a protocol bug. Gemini 3.x attaches a thought signature to every function call and refuses the next request if it is missing. My hand-written loop rebuilt the model's turn from {name, args}, dropped the signature, and every tool-using run died at step two. Moving the loop onto Strands removed that whole class of bug: the SDK carries the signatures and the tool-result turns itself, and my runtime went back to being about the fleet instead of the wire format.

Moving to Strands late. I rebuilt the runtime on Strands in the final days. The SDK needs @google/genai 2 and zod 4, and zod 4 changed how object defaults behave, which broke the editing timeline's schema until every nested default became a prefault. The honest part: the big 54-step run described below happened on the previous hand-written loop, and I have not yet re-run a full production day end to end on the Strands runtime.

Imagen does not exist on this account. Five published model ids, five 404s, and the Gemini image models refuse the endpoint the SDK's generateImages() calls. So no character reference image had ever been generated, and every shot invented a new face. The fix was generateContent with image output — which is also what makes reference images an input, and the only reason the storyboard exists.

The videos kept lying. A shot directed as "an insert on the screen" came back with a fabricated Shopify dashboard. A selfie came back with an iPhone camera app painted over it, mode tabs reading FENTO VIDEO MATEO PHOTO FORMOTO. The model did not classify a user interface as text. The negative list is a bug log now: every entry names something actually seen in a rejected frame.

Cloud Run withdraws the CPU when the request returns. Renders were fire-and-forget. A render left running there does not fail — it freezes at whatever percent it reached, for ever. The CLI had always awaited the render queue, which is exactly why rendering worked on my laptop and never once in the cloud.

And the silence. Omni's clip audio stops when the speech stops, so a shot held past its last word fell to \( -90 \) dBFS. There is a synthesised room tone under everything now, about \( -46 \) dBFS, crossfaded so it loops without a seam — inaudible as a sound, audible as the absence of a hole.

Accomplishments that we're proud of

The run I keep going back to is 54 steps long. The agent read its memory, chose a shape, produced the video, ran the timeline check, saw the warnings — opening silence, a stretch with no visual break — and fixed them itself. Added a sound effect. Applied three effects. Re-checked until the issue list was empty. Rendered. Scheduled the post with a reason about a lunchtime scroll window. Wrote what it learned. Nobody told it to do the middle part. That is the difference between an agent and a pipeline, and it is in the trace.

Measured rather than claimed:

  • A 22-second video at 1080×1920 and 60fps, with audible sync dialogue and word-level captions, produced end to end on Cloud Run in 623 seconds.
  • 22 scenarios across 16 distinct shapes.
  • A character who is recognisably the same person across every shot of a video.
  • The vision judge catching a fabricated interface before it cost a clip.

And the honest one: zero videos have been published. Publishing is implemented and tested in dry run, and the founder's switch has not been thrown. The decision engine has never seen a real retention curve.

What we learned

Move every rejection to the cheapest stage that can catch it. Let \( c_i \) be the cost of a rejection at stage \( i \) and \( p_i \) the probability of catching a given defect there:

$$\mathbb{E}[\text{waste}] = \sum_i p_i\, c_i$$

The game is shifting probability mass toward small \( c_i \). A wrong frame caught after generation costs about €0.60 and forty seconds; caught as a storyboard panel, about €0.13 and three. That ratio is why the storyboard exists.

Put the protocol in the framework and the judgement in your code. Days of my time went into thought signatures and turn shapes. Strands made that disappear, and what was left in my runtime was the part that is actually mine: what gets recorded, what counts as failure, what money is allowed to buy.

A creative step that fails quietly is worse than one that fails loudly. My scene writer once swallowed every error into a convincing template fallback, and nobody noticed for days that the model had stopped writing.

Green is not proof. Typecheck passed on every commit of a system that had never produced a watchable video. I only found what was wrong by extracting frames and measuring decibels.

And the one I did not expect. I stopped learning to code because I decided a model would do it better than me. Three years later I built a system whose central design decision is taking work away from the model — closed vocabularies instead of free prompts, rules in code instead of in instructions, facts behind functions. The value sits in knowing precisely what to ask for.

What's next for OttoUGC

  • Run a full production day on the Strands runtime, then throw the publishing switch: one channel, one video, one real retention curve.
  • Use Strands interrupts for the decisions that belong to a human: approving a new creator, confirming a number the brief couldn't source, raising the budget.
  • Deploy the agents on Amazon Bedrock AgentCore, so each creator runs in its own session with its own memory instead of inside one web worker.
  • Multi-tenant onboarding, and paid amplification for the videos the grid marks as outliers, which is where the economics close.

I killed Casanova because I believed a good product would find its own customers. This is the agent I built after finding out it won't.

The woman in the kitchen has a voice now.

Built With

  • chirp
  • cloud-build
  • cloud-scheduler
  • cloud-sql
  • drizzle-orm
  • gemini
  • gemini-omni-flash
  • google-cloud
  • google-cloud-run
  • lyria
  • nano-banana
  • next.js
  • node.js
  • postgresql
  • react
  • remotion
  • speech-to-text
  • strands-agents
  • tailwindcss
  • thompson-sampling
  • typescript
  • vertex-ai
  • youtube-analytics-api
  • youtube-data-api
  • zod
Share this project:

Updates

Submission history