MyJourney: create product demos without wasting a day

Table of Contents

GemLab has several projects running at the same time, and all of them change from week to week. Then someone asks how Mother is doing, and the honest answer is to share a screen and click through it together.

Recording a real demo solves that once. Script, take, a mistake halfway through, re-record, cut, upload. A full day of someone’s work. And on the next deploy, the screen has changed, and the demo is stale.

Multiply that by several projects, and you understand why nobody re-records.

That’s why myjourney exists: you give it a URL and a description of what you want shown, and it hands back a narrated MP4. It drives a real browser through the site, with a visible cursor, and narrates what it’s doing.

It’s still at a very early stage, so expect rough edges. But the end-to-end path already works.

What it does

You fill out a form: the URL, the description, and optionally a logo and a cover. A few minutes later there’s a video in the gallery. The path between one and the other is this:

The myjourney flow: from URL and description to an MP4 in the gallery, passing through planner, JSON plan, human approval, recording, narration and assembly, and quality gates.

The planner explores the site in a real browser, the way a visitor explores: it navigates, reads, and tries things out before promising a scene on top of them. When a plan builds a beat around the site’s search, it’s because it typed into that search first and watched the autocomplete respond. At the end, it writes the plan: the tour’s beats, in JSON.

The plan stops and waits for a person to approve it. Planning is fast and cheap; recording holds a browser and an ffmpeg encode for several minutes. So the route gets written first and sits in the queue until someone says it’s worth recording.

Once approved, a worker takes over. The plan compiles to a Playwright script, which drives the browser. The recording comes from the CDP screencast, Chromium’s DevTools protocol, which emits a frame on every page paint. And the cursor you see in the video doesn’t exist as far as the system is concerned: synthetic input has no pointer, so the cursor is a DOM element we inject and animate. At the end, the narration becomes voice and ffmpeg assembles the cut with soundtrack and watermark.

Under the hood, the app is deliberately small: two processes and a database in a single file. No broker, no external queue, the queue is a table, an event is a row.

App architecture: a web server writes jobs to a single database and the page follows along over SSE; a worker picks up jobs with a lease and writes progress back.

The server writes a row to the database and responds. The worker picks up the job with a lease and keeps writing progress. The client page follows along over an event stream that reads those same events from the database. Either process can die and come back without the other noticing.

The recording itself runs on top of argo, which turns a Playwright script and a scene manifest into a narrated video. myjourney supplies the half that argo assumes a human wrote: looking at a site it has never seen and deciding what a tour of it should be.

A public example, which also opens the myjourney42.cm42.io home page: the tour of the Codeminer42 website. Nobody wrote a script, drew a storyboard, or opened an editor:

Watch the end of the video: it actually fills out the contact form. Nobody told it to; the form was there, and it was the natural next step for whoever is watching.

Where the model doesn’t decide

That contact form filled out with nobody asking is charming on a public site. Now imagine the same enthusiasm applied to a delete button.

That’s why the beat that clicks declares its own risk, and validation re-derives that risk on its own, and refuses to click anything matching delete, buy, checkout, cancel subscription, invite, or sign out, no matter what the plan declared. Hovering over a delete button to show it exists is fine. Pressing it is not.

The model does not self-certify. Ever.

And then there’s the other boundary: most products worth a demo live behind a login. The marketing site is open, but the thing being sold is behind a form. A tour that only sees what a stranger sees is a tour of the wrong half.

Logging in is a person’s job. npm run login opens a real browser, you sign in however the app asks, password, SSO, 2FA, magic link, hardware key, and press Enter. Everything works, because the one doing it is a human and not a script guessing at selectors. No password is typed, stored, or seen by a model.

What it figured out on its own

That’s why the first demo we recorded for real was of a logged-in app: Mother, a project of ours that sits between your AI agent and your main branch. It’s the kind of product that’s hard to explain in text and easy to understand by watching.


The plan myjourney wrote for Mother has seven beats and 58 seconds. The arc goes like this:

BeatWhat happens
establishMission Control, the daily view of open work
surveywhat each project reports there
traversethe product’s second area, which opens filtered to you
demonstrateswitches the filter to "All"
revealthe whole account’s work shows up at once
traversea third area of the product
closecloses back on Mission Control

Look at that last beat. The narration it wrote was: "The loop closes in Mission Control, where the next gate appears as soon as your agent opens a pull request."

It found the product’s loop and made it the tour’s loop. Nobody asked for that. What we gave it was a URL, a description, and a logged-in session.

A frame from the tour: Mother's Mission Control in dark theme. The cursor drawn by myjourney, an arrow with an orange ring around it, rests over the link for the geeksilva97/myjourney project. The card shows PR number 1, "The app: a queue, a worker, and a route you can edit", gate Clear, reviewed by Edy Silva 19 hours earlier, and the four capabilities underneath: gate, review, assessment and context. In the bottom-left corner, inside a circle, the video's presenter speaks the narration.

Look at the card the cursor is pointing at: it’s myjourney, with its PR number 1. Mother is governing myjourney while myjourney records Mother’s demo.

The model writes JSON, never code

The model found the product’s loop by itself. You could conclude that from there on you just trust it. But there’s one thing it never writes in this pipeline: code.

The obvious approach would be to ask the model for the Playwright script directly. It writes JavaScript well, Playwright is well documented, and it works on the first try.

On the tenth it hands you a file with a syntax error. Or worse: a file that runs and does something you didn’t ask for.

That’s why the model here doesn’t write code. It writes JSON, and a compiler we wrote turns that into Playwright. The generated file is a thin shell around a literal:

const PLAN: TourPlan = { /* ... */ };
test('tour', async ({ page, narration }) => {
  await runTour(page, narration, PLAN);
});

All behavior lives in tour-runtime.ts, which is hand-written and tested. A bad plan is a validation error, never a syntax error in a file someone has to debug at 3 a.m.

And there’s the better part, which is what the model cannot say. A goto can only reach a path on the tour’s own origin. A key can only be one from a closed set. The DSL has no way to express "open another tab and log in somewhere else", because there is no verb for it.

It’s a restaurant order slip. The waiter takes the order; the kitchen decides how it cooks. And the waiter can’t write down a dish the kitchen doesn’t make.

The bill for this is expressiveness. A new capability isn’t a prompt tweak, it’s a change to the compiler and its tests. When the domain is genuinely open, letting the model write code wins. Here the domain is closed on purpose, so the DSL wins by a wide margin.

It worked on day one, and the video was worthless

With the compiler holding the syntax, the render pipeline stood up on the first day. And the first videos were voice over a screenshot, not as an impression, as a measurement: 46 to 54% of every video was a still frame. One opening had eight seconds of a motionless home page with a voice talking over it.

The cause wasn’t a bug. It was structural.

The narration set the beat’s duration. Each action took however long it took. And the leftover became a hold at the end. Nothing stretched, so slack became a photo by default.

That’s why every beat today declares a shot, what it exists to do, and has an elastic action that grows to fill whatever narration is left. drift scrolls the page continuously, sweep sends the pointer visiting several targets, splitting the time between them, push is camera movement in ffmpeg and works even on a page that animates nothing.

hold still exists, capped at 800ms, because hold was the problem.

The result: the video moves almost the whole time, and that’s what makes it look like a tour someone recorded instead of a narrated slideshow.

Allocation is decided live, not precomputed, because there’s no way to know what a click costs before clicking. If a navigation drags, the following actions shrink instead of blowing past the narration.

A job can finish successfully and be worth nothing

The video started moving. But the pipeline kept reporting success on videos that were no good. Hence four quality gates before publishing, and each one was born from a video that passed everything and wasn’t usable:

  • shape: the compiler’s validation, the one that never becomes a syntax error;
  • composition: it is structurally a demo. It opens by establishing, closes on a next step, and has at least one demonstrate followed by a reveal. Without that pair you have a narrated screenshot gallery, which is exactly what the first three samples were;
  • motion: at least 70% of the recording has something moving, measured per beat from the video;
  • pixels: blank-frame detection for a site that didn’t render, and text signatures for bot challenges.

The fourth gate has the best story. google.com sailed through the blank-frame test while serving a reCAPTCHA, because a CAPTCHA page is full of content. It took a gate that reads what’s written, not how much is painted.

And the gates have a ceiling, which I found out reviewing one of the tours for the fifth time. A beat lands on a screen that has no data yet, an empty state, with the narration promising the feature on top of it. motion passed, because the pointer travels and the camera pushes. composition passed, because the arc is right. Neither one can tell whether the screen had anything to show.

A gate measures what can be measured. "This screen is worth showing" is still a human’s judgment.

A frame from the tour: Mother's Work Feed with the All tab active. The cursor's ring highlights a row in the list, which shows gates from several projects, myjourney PR 1 "The app: a queue, a worker, and a route you can edit" with 8 topics and Done, mother PR 108 "Review-request guardrails + Mission Control capability redesign", myjourney42 PR 24 "fix: serialize renders + retry flaky narration (prod CPU/RAM saturation)". Across the footer, the overlay the tour wrote itself: "Every gate, every agent, one feed". In the bottom-left corner, the presenter.

The models and services in the pipeline

If the final judgment is human, everything else in the pipeline is machine. And a machine has a name and a price on the label:

StageWho runs itWhere it runsCost per tour
PlanningClaude Opus, with browser toolsAPIexploration tokens
English voiceKokorolocal$0
Portuguese voiceminimax-speech-02-hdRunPod$0.0276
Presenter portraitFLUX.1-devRunPod$0.012
Presenter lip-syncInfiniteTalkRunPod$0.25
Recording and assemblyPlaywright, CDP and ffmpeglocal$0

A complete tour, with a presenter speaking, comes out at around thirty cents plus the planning tokens.

Planning is the only place a large model comes in: a Claude Opus with browser tools. On one of the first tours, it took 18 exploration tool calls for seven beats that passed validation on the first attempt.

The English voice is Kokoro, an open TTS that runs locally, no API key. The Portuguese voice was more fun: Kokoro does ship the weights for a Brazilian voice, but the JavaScript package blocks in three layers before reaching them, the last one is the phonemizer, which only speaks English. So Portuguese is narrated by minimax-speech-02-hd, on a RunPod endpoint, for the cents the table shows.

A nice side effect: a tour in another language isn’t a translation. The plan is redone from scratch in the target language, and out comes a different tour; the Portuguese planner found a path through the site the English tour never touched.

The presenter in the corner is two models. The portrait comes from FLUX.1-dev; the lip-sync is InfiniteTalk, open (Apache 2.0), on a public RunPod endpoint, $0.25 per render at 480p, regardless of duration. It’s driven by the narration audio, not the text: the mouth stops moving exactly when the voice stops, because at that point the audio is silence. And because it’s audio-driven, it’s language-agnostic by construction; the Portuguese tour didn’t need a single line changed at this stage.

A frame from the Codeminer42 tour: the site's services section with the drawn cursor over the text, an overlay reading "Onboarding support and flexible staffing" at the foot of the card, and in the bottom-left corner, inside a circle, the generated presenter speaking the narration.

And the pipeline accepts your own material. Don’t want a generated face? Upload a portrait of yourself, and that’s who speaks the narration; the lip-sync costs the same $0.25. Branding comes in too: an opening cover, a logo in the corner as a watermark, and background music under the narration. It all goes in along with the URL, and ffmpeg burns it into the final cut.

None of this was the first option that showed up. There was a survey first, what existed in open and commercial models, and measured experiments after. The family that repaints mouths, Wav2Lip, MuseTalk, LatentSync, was out because it dubs existing video, and all we have is a still portrait. WAN 2.6 was compared side by side and lost because it won’t shut up: during the narration’s pauses it keeps chewing air, at 0.56 motion in silence against InfiniteTalk’s 0.28. Those experiments became lab notebooks in the repo, with costs and measurements written down.

That research was done the way we work in the lab: with Claude. Claude Code ran the model survey, wrote the reports that became the basis for the choice, and ran the experiments that produced the numbers above. I’ve already written about how I use it day to day in Claude Code: the irrational effectiveness of simplicity.

The demo became a build artifact

Back at the start, the question was "how’s Mother doing?". Today the answer is a one-minute MP4 that cost about thirty cents, and that can be re-recorded tomorrow, when the screen changes.

That’s what changes the picture. When a demo costs someone’s day, it’s an event: we put it off, it ages, and back comes "let me share my screen". When it costs cents and nobody’s hours, it becomes a build artifact, the interface changed, re-record, just like running the test suite after a commit.

And that’s what we’re working on now: optimizing the pipeline so that re-recording and swapping pieces of the video, a scene, a narration, a brand, gets cheaper and cheaper, without paying for the whole path again. And longer tours too, because right now the ceiling is one minute. It’s Kent Beck’s old advice: make it work, make it right, make it fast. Making it work was the first step; we’re in the middle of the list.

myjourney is live at myjourney42.cm42.io, in that early stage I warned you about. GemLab sums up the thesis in a better sentence than mine: building an MVP became trivial; having real software is still hard.

That’s it for today.

We want to work with you. Check out our Services page!