Viral video prompts
A product and one sentence in, a finished video out. You drive the steps; the repository holds the evidence rules that keep the result honest.
Run everything from the repo root. node run.mjs and every path below are
relative to that checkout.
Before anything — ask, do not guess
run.mjs asks these itself when it has a terminal. You usually do not, so ask
them in conversation first and pass them as flags. Each one guessed wrong costs a
paid generation.
1. What is this product? One of three, and it is not a formality — it changes what gets read and whether there is an end card at all.
--context-kind | --context | what it means |
|---|---|---|
landing | https://… | a shipped page; the end card records it |
github | https://github.com/owner/repo | the README and code are the description; the end card records the repo page |
local | /path/to/project | unshipped. Nothing is fetched, and there is no end card — say so rather than inventing a page |
2. Which model. Default minimax/hailuo-3 — 2K, 5–15s per call, speaks its
own narration. --model takes any id from
https://openrouter.ai/api/v1/videos/models; the run validates the choice
against that catalogue before discovery rather than at generation time.
3. How long and which way up. --length <seconds> and
--orientation portrait|landscape. Ask which platform the video is for and
derive it — portrait 9:16 for TikTok/Reels/Shorts, landscape 16:9 for YouTube,
embeds, a site hero. Beyond the model's per-call cap the plan is split at a
narrative seam, and the segments will not match across the cut.
4. A draft first? --draft renders one 10-second segment at the cheapest
resolution before anything expensive happens. Offer it — on hailuo-3 it is $0.34
against $1.30.
And these, which nobody can supply but the person:
--summaryone sentence: what it does, for whom--audiencewho the video is for--claim(repeatable) what the product may truthfully say about itself--prohibited(repeatable) what must NEVER be claimed
The last two matter more than they look. Every spoken line is checked against them, so a product with no stated claims can have anything said about it.
Ask which market and which language. If the landing page serves several languages
from one url, record the end card with --language, or the video closes on a
page its viewers cannot read.
1. Write the query file — do not skip this
Before discovery, write queries/<slug>.json with at least 12 queries across
6 angles. queries/example.json is the template.
One narrow angle returns one narrow kind of video. A first run using only "pet + AI + gadget" surfaced nothing but sound-to-words translators and companion robots, none of which were usable.
Angles that earn their place: category · workflow · pain (how a user
describes the problem in their own words) · competitor (name real ones) ·
result_proof ("I tested…") · format_native (what this category's viral videos
actually look like).
2. Discover, and prove the traffic
node run.mjs --name <Name> \
--context-kind landing|github|local --context <url-or-directory> \
--summary "..." --audience "..." --claim "..." --prohibited "..." \
--model minimax/hailuo-3 --length 15 --orientation portrait \
--queries queries/<slug>.json
Everything is yt-dlp. No browser, no login, no daemon.
YouTube gets a real phrase search across three surfaces. TikTok does not have one — yt-dlp ships no free-text TikTok search — so a query reaches TikTok three other ways, and which one produced a candidate is recorded on it:
| Query written as | Surface |
|---|---|
pet mood app | resolved to #petmoodapp, hashtag listing |
@handle | that creator's page |
https://www.tiktok.com/@who/video/123… | that one video, with real metrics |
Treat these as different instruments, because they are. When TikTok comes back thin, that is the reason — say so, and reach for the third row: a reference you found by hand, in any browser, pasted in as a query enters the corpus as a seed and is held to exactly the same evidence rules as anything the tool found itself.
Listing cards carry no metrics, so TikTok candidates are probed individually afterwards.
Expect TikTok to be flaky. yt-dlp's TikTok extractors are rate-limit
sensitive: the same video URL can return full metadata and, an hour later,
Unable to extract universal data for rehydration. Read preflight.json in the
run directory before you tell anyone what TikTok does or does not have — it
records both surfaces separately, with the actual stderr. Report "TikTok was
unreachable this run", never "there are no TikTok references".
Instagram and Douyin have no adapter at all: their absence is a tooling limit, never evidence that no reference exists, and must be reported that way.
3. Shortlist — the step where judgement is actually required
The run ranks and stops. Take at most 5 per platform. Rank by traffic within a platform, never across. Then apply the rule that matters most:
High traffic without category fit is never the primary reference. Fit without traffic evidence is not proof of virality either.
A first run's top-viewed candidates were a 553M-view animated short and a 33M-view comedy sketch — entertainment whose traffic belongs to the subject and the channel, not to any transferable format.
4. Judge fit — and judge the right thing
Choose the semantics of "fit" deliberately, because this single choice reverses results:
- Functional equivalence — "does the demoed product do what ours does?" Correct when you need a direct competitor's demo.
- Structural transfer — "does this video's shape carry to our product?" Correct almost every other time.
Under functional equivalence one run correctly rejected all five candidates, including one at 29.4M views, because none of them did what the product did. Under structural transfer that same video passes cleanly — an owner, her own pet, a visible capture gesture, a per-subject result, and the owner adjudicating that result against the live animal in the same shot. What transfers is structure, rhythm, shot grammar and music feel. What never transfers is a claim.
Write the definition you chose into the brief, so the judgement is recorded rather than improvised.
5. Extract structure, then write beats
From the chosen reference, extract two lists and keep them apart:
- transferable — concrete structural moves, with timestamps
- doNotCopy — claims the product cannot make, plus footage, likeness, branding, verbatim copy, and any competitor's name
Then write the product's own beats. Each beat carries: timing, role, what is on screen, the exact spoken line, the caption, and which transferable move it implements. Add a claim check: every spoken line names the verified claim that backs it, or is marked "reaction only".
Reuse the reference's credibility devices, which are usually what makes it work: state the doubt before the viewer can, show one failure or retry rather than a flawless streak, and close on the subject rather than the product.
A beat plan is JSON:
{
"visualGrammar": "handheld, pet eye-level, one room, natural light",
"musicDirection": "none until 6s, then a soft loop under the reaction",
"beats": [
{ "startSec": 0, "endSec": 3.5, "role": "hook",
"onScreen": "owner crouches, phone up, dog mid-yawn",
"narration": "I did not think this would work.",
"caption": "day 1", "implementsMove": "doubt stated before the demo" }
]
}
6. Draft, then generate
Draft first if the person said yes, and offer it again here if they did not:
node run.mjs --generate --plan <beat-plan.json> --draft
One segment, ten seconds, cheapest resolution. On a model with no cheap tier —
hailuo-3 renders 2K or nothing — the draft switches to bytedance/seedance-2.0-mini
at 480p and says so. Tell the person what that draft does and does not show:
structure, pacing and whether the narration lands, from a different model. Not
the look of the final render.
Then the real thing:
node run.mjs --generate --plan <beat-plan.json> --model minimax/hailuo-3 --orientation portrait
Narration goes into the generation prompt — the generator speaks it natively. There is no separate voice synthesis step and no TTS voice to pick.
Every model caps one generation, hailuo-3 at 15 seconds. Longer videos are split at a narrative seam, never at an arbitrary time. Segments are generated independently, so the subject and the room will not match across the seam: either accept it as a cut, or keep each segment self-contained.
Check the price before committing — --max-cost-usd refuses rather than
surprises. Verify what came back really has an audio track (ffprobe, or
src/produce/qa-video.mjs) instead of assuming it.
7. End card
Record the real page with src/produce/capture-ui.mjs; never generate a fake
one. Put the text where it does not cover the page's most persuasive element.
Pass --language to match the video's language.
Which page depends on question 1: the landing page, or the GitHub repo page. A
local product has no page, so it has no end card — close on the subject and
say the product has not shipped, rather than mocking up a site that does not
exist.
Never
- Never publish. This delivers a file; a person posts it.
- Never let a spoken line outrun the product's verified claims.
- Never present a high-traffic, low-fit video as a primary reference.
- Never report a missing search adapter as "no references found".
- Never present a TikTok hashtag listing as if it were a phrase search.
- Never let a draft from a substitute model stand in for how the real model looks.
- Never commit third-party video. Metadata, analysis and hashes only.