Inspiration

I started from a suspicion and tried hard to kill it: that editors are given audience data at a resolution too coarse to act on.

It survived. The vendors say so themselves, and Brightcove says it most plainly because its docs are technical rather than promotional: "we divide the video into 100 equal parts", with the underlying event firing every ten seconds. Kaltura reports quartiles: 25, 50, 75, 100%. Vimeo's marketing page says "second-by-second", but its help docs concede the aggregate uses 100 equal segments. One hundredth of a 52-minute episode is 31 seconds. A scene is four minutes. You cannot see a scene in that.

Then I looked at the tools editors actually sit in front of. Frame.io, Avid MediaCentral and Blackbird carry no audience data at all. Not a coarse version. None. No vendor even claims otherwise. Frame.io's entire vocabulary is comments and review links.

So the data exists. Netflix's engineering blog puts its playback layer at 15 million events a second. And the people who could act on it have nothing. Both halves sit inside the same company and never meet.

That gap matters more than it used to. Scripted originals peaked at 599 in 2022 and fell to 516 in 2023, the first decline FX ever recorded. Ampere Analysis puts average order-to-release at 404 days, up from 288 in 2020. And a season of Stranger Things ran $30 million an episode. Fewer shows, costing more, taking longer, and the feedback loop that should inform the next one stops at "episode four underperformed."

What it does

Cutaway holds two tables in ClickHouse: 17 million playback events, and the scene breakdown that already falls out of the edit as an EDL. It joins them on the playhead:

position_sec >= start_sec AND position_sec < end_sec

Half-open, always. A closed interval double-counts everyone who quits exactly on a scene boundary, and boundaries are where people quit, so the error lands hardest on the rows the product is about.

Then an agent answers questions in an editor's language. Not "show me a chart", but "where are we losing them, and is it the same thing twice?" It writes its own SQL, tries to disprove its own finding, and replies in scenes and timecodes:

INT. RECORDS BASEMENT - NIGHT, episode 4, 06:00, nine minutes, 11,616 lost.

The console has four views. Season is a heatmap: eight episodes down, 52 minutes across, every abandon at the minute it happened. Episode puts the retention curve over the cutting timeline so the fall lands inside a named scene. Scenes is the breakdown as a document. History keeps every question and the SQL that answered it.

How I built it

Google Cloud: Gemini through the Agent Development Kit, on Vertex AI, deployed to Cloud Run with the database password in Secret Manager. Partner: ClickHouse Cloud, reached through the official mcp-clickhouse MCP server.

The interesting decision was architectural. It is four agents, and the first three are an ADK SequentialAgent:

  • locate ranks every scene and must state which population it ranked
  • verify is handed that finding and attacks it from two other angles: loss per minute rather than total, and whether it generalises by scene kind
  • report writes the answer. It has no database access at all, so it can only write from what the first two established
  • checker is a separate agent that sees only the SQL and the draft, and decides whether the sentences are entailed by the queries

Challenges I ran into

The agent kept lying with true numbers. A single agent, told in detail and with the failure named, ran this:

... WHERE s.kind = 'cold_open' ORDER BY abandons_per_sec DESC LIMIT 5

and wrote "the five fastest losing scenes in the season are all cold opens".

The figures were right. The population was not. It had ranked cold opens, then reported the result as though it had ranked everything.

I strengthened the instruction. It did it again with a different scene kind. I named the exact failure in the prompt. It did it a third time.

That's what turned this from one agent into a pipeline plus a checker. Telling a model to check its work is not the same as checking it.

Then the checker over-corrected. It started treating LIMIT 5 as a filter and hedging claims that were perfectly fine. A checker that makes answers vaguer has made them worse, not safer. Two worked examples in its prompt sorted it: it now passes fair claims untouched and narrows only when it can point at the WHERE clause.

A dependency conflict that reads like a bug in someone else's code. mcp-clickhouse needs the MCP 2.x SDK; ADK pins mcp>=1.24,<2. Install both in one environment and ADK dies importing mcp.shared.session. An MCP server runs as a separate process anyway, so it gets its own virtualenv. The Dockerfile builds two.

My own data had a tell. The words per shot column read 73 for every dialogue scene and 4 for every other one. Two constants wearing a distribution's clothes, because I had derived shots and words from the same boolean. A judge poking at that column would have found it in a minute. Both are now drawn per scene, so the spread is real: 52.8, 46.2, 32.0 against 1.3 to 2.1.

Accomplishments that I'm proud of

The moment the pipeline earns its keep is when verify overturns locate. Asked which scene loses viewers fastest, locate returned the largest total loss: episode 3, 11,705 viewers. verify measured per minute and found a different scene, episode 8's cold open, at 1,485 a minute. The report gave the editor both and let them choose, instead of quietly picking the number it liked. I verified every figure by hand against ClickHouse.

Also: the season heatmap, which makes the argument before anyone reads a word. And that the console draws from the same joins the agent writes for itself, so the chart and the answer cannot disagree.

What I learned

That prompting is not a control surface. I have three logged instances of an agent ignoring an instruction that named its exact failure. What fixed it was structure: a step that always runs, and a second agent looking at different information.

Also that surveying the competition properly beats assuming. I asked for the research to be adversarial, to tell me plainly if editors already get scene-level data, because then there is no product. It came back the other way, in the vendors' own words, and those quotes are now the strongest slide in the deck.

What's next for Cutaway

Ingest a real EDL or ALE so the scenes table comes out of an actual edit rather than a generator. Push findings back into Frame.io as timestamped comments, so the note lands where the editor already works. And per-cohort retention: the same join, sliced by how a viewer arrived, which the schema already supports.

Built with

google-adk · Gemini on Vertex AI · Cloud Run · Secret Manager · Artifact Registry · ClickHouse Cloud · mcp-clickhouse (Model Context Protocol) · FastAPI · Python 3.13 · plain JavaScript with hand-drawn SVG, no chart library and no build step

Built With

  • cloud-run
  • gemini
  • google-adk
  • vertex
Share this project:

Updates

Submission history