The first three seconds of a UGC ad decide whether a thumb stops scrolling. Everything after is irrelevant if the hook fails. Hook Machine is the tool I built on the video generation platform to generate, score, and rank 3-second ad openers — not full ads, not 15-second spots, specifically the interrupt moment that earns the next twelve seconds of attention.
Twenty-one merged PRs took Hook Machine from research roadmap to production tool. This post covers the hook scorer algorithm, prompt principles, content-aware engine routing, karaoke captions, seed frame dispatch, pre-spend guards, and the four UX workstreams that shaped the editor experience.
A hook is not a shorter ad. It is a different artifact with different constraints: interrupt-first, in-budget, maximum motion, zero setup time.
The 3-second hook problem
Full ad generation optimizes for narrative arc — hook, body, close. Hook generation optimizes for a single metric: pattern interrupt in under three seconds. That changes every design decision:
- Duration ceiling — hard cap at 3000ms; no scene transitions within the hook
- Copy budget — 8–15 words max; longer copy guarantees VO overrun
- Motion priority — static product shots fail; hooks need movement in frame one
- Ranking — editors generate 5–10 candidates and need best-first ordering, not chronological
| Dimension | Full ad generation | Hook Machine (3s openers) |
|---|---|---|
| Target duration | 15–60s | ≤3s |
| Primary output | Stitched multi-scene video | Ranked hook clips |
| Editor workflow | Script → plan → generate → stitch | Brief → batch generate → score → pick winner |
| Cost per candidate | $1.50–$4.00 | $0.15–$0.80 |
| Success metric | Ad completion rate | Thumb-stop proxy score |
Refocus and infrastructure hardening
The first major PR — feat/hook-openers-refocus-and-hardening — stripped Hook Machine down to 3-second openers only. Earlier prototypes tried to be a general short-form generator; refocus eliminated scope creep and let every subsequent PR optimize for the hook use case. Full infra hardening followed: structured error types, retry policies, Docker build parity, and shared logging with sibling tools.
Roadmap docs (docs/roadmap-from-video-gen-research, docs/roadmap-readiness-and-p1-spec) translated video-gen research into a P1 spec before code shipped. Lessons from prior video-gen failures — documented in docs/lessons-from-video-gen — became explicit guardrails in the spec.
Hook scorer: rank candidates best-first
P1's core feature (feat/hook-scorer-ranking) scores each generated hook and returns candidates sorted best-first. The scorer combines heuristic signals — no ML model training required:
interface HookScoreSignals {
motionDensity: number; // optical flow magnitude, normalized 0–1
copyFit: number; // spoken duration vs 3000ms ceiling
firstFrameImpact: number; // contrast + edge density in frame 0
audioPeakTiming: number; // first emphasis within first 800ms
captionReadability: number; // chars visible per second
}
function scoreHook(signals: HookScoreSignals): number {
const weights = {
motionDensity: 0.30,
copyFit: 0.25,
firstFrameImpact: 0.20,
audioPeakTiming: 0.15,
captionReadability: 0.10,
};
return Object.entries(weights).reduce(
(sum, [key, w]) => sum + signals[key as keyof HookScoreSignals] * w,
0,
);
}
function rankCandidates(hooks: GeneratedHook[]): GeneratedHook[] {
return hooks
.map(h => ({ ...h, score: scoreHook(analyzeHook(h)) }))
.sort((a, b) => b.score - a.score);
}
Motion density is the highest-weight signal because static hooks lose in feed environments. Copy fit penalizes hooks where TTS VO exceeds 3 seconds — a common failure when the LLM writes 25-word hooks for a 3-second slot.
Prompt principles: interrupt-first, in-budget, more motion
feat/hook-prompt-principles codified three rules in the prompt template:
- Interrupt-first — open mid-action or mid-sentence, never with setup ("Hi, I'm…")
- In-budget — copy must speak in ≤2.8s at target WPM; template enforces word count
- More motion — camera or subject movement in the first frame; static = reject
const HOOK_PROMPT_TEMPLATE = `
Generate a 3-second UGC ad hook. Rules:
- INTERRUPT: start mid-action, no greetings or introductions
- BUDGET: exactly {{wordCount}} words, must speak in under 2.8 seconds
- MOTION: subject or camera must move in frame 1 — no static product shots
- PRODUCT: {{productName}} visible within first 500ms
- TONE: {{tone}} — match target demographic
`;
Content-aware routing
Not every hook should hit the most expensive video engine. feat/content-aware-routing inspects the generation brief and routes to the cheapest engine that can handle the content:
| Content profile | Engine | Rationale |
|---|---|---|
| Product-only, no human | Seedance | Cheapest for object motion |
| UGC presenter, talking head | Kling | Better lip sync + identity |
| High motion, lifestyle | fal Veo | Best motion quality |
| Policy-sensitive category | Replicate fallback | Looser content policy |
Content-policy self-healing: when an engine rejects content (422 or policy block), the router automatically retries with the next engine in the fallback chain — no editor intervention required.
async function routeWithSelfHeal(
brief: HookBrief,
engines: EngineChain,
): Promise<GenerationResult> {
for (const engine of engines.orderedFor(brief)) {
try {
return await engine.generate(brief);
} catch (err) {
if (isContentPolicyRejection(err)) continue;
if (isTransient422(err)) {
await backoffRetry(() => engine.generate(brief));
return /* result */;
}
throw err;
}
}
throw new AllEnginesRejectedError(brief.id);
}
Pre-spend guards
feat/pre-spend-guards validates before any API spend fires:
- Copy-length preflight — estimate spoken duration from word count + WPM; block if >3s
- Clip health check — verify seed frame resolution, aspect ratio, and file integrity
These guards saved meaningful spend during beta — editors pasted long product descriptions that the LLM condensed poorly, producing 5-second VO for 3-second hooks.
Seed frame dispatch chain
P3 (feat/seed-frame-dispatcher) added high-quality seed frame generation with a fallback chain:
- Nano Banana Pro (Gemini) — primary, best look-match from product catalog
- gpt-image — fallback on Gemini timeout
- Seedream — last resort for policy-blocked categories
Seed frames anchor the first video frame. A bad seed produces a bad hook regardless of prompt quality. Dispatch order optimizes for quality first, with automatic degradation on failure.
Karaoke captions
P2 (feat/karaoke-captions) renders word-level timed captions using libass, with drawtext FFmpeg fallback when libass fonts are unavailable in the container:
async function renderCaptions(
hook: GeneratedHook,
words: WordSpan[],
): Promise<string> {
if (await libassAvailable()) {
return renderLibassKaraoke(hook.videoPath, words, {
font: 'Inter Bold',
highlightColor: '#64ffda',
position: 'bottom-center',
});
}
return renderDrawtextFallback(hook.videoPath, words);
}
Karaoke captions serve double duty: they improve thumb-stop (text movement draws the eye) and feed the caption readability signal in the hook scorer.
Four UX workstreams
Hook Machine's editor UX shipped as four sequential workstreams:
| Workstream | PR | Capability |
|---|---|---|
| PR-A: Incremental reveal | feat/ux-incremental-reveal |
Results appear as each candidate completes; live progress + cost tally |
| PR-B: Persistence | feat/ux-persistence-history |
Recent runs + ?job= URL resume after tab close |
| PR-C: Winner loop | feat/ux-winner-loop |
Per-variant actions: regenerate, promote, decision log |
| PR-D: Smarter input | feat/ux-smarter-input |
URL→brief extraction + saved product library |
The winner loop pattern
The winner loop is Hook Machine's core retention mechanic. After scoring ranks candidates, the editor:
- Promotes the top scorer to "winner" status
- Regenerates variants near the winner with mutated prompts
- Logs decisions — which hook shipped, why, and what was rejected
Decision logs feed back into prompt tuning. If editors consistently reject high-motion hooks for a product category, the prompt template adjusts motion emphasis downward for that category.
Drive reliability
feat/drive-reliability-rca added RCA tooling for Google Drive upload failures — structured error codes, retry with chunked upload, and admin-visible failure reasons. Hook clips are small (3s ≈ 2–5MB) but batch runs of 10 candidates stress concurrent upload paths.
What I'd do differently
I would integrate the hook scorer into generation — aborting candidates that fail copy-fit preflight before video generation, not scoring them after spend. Post-generation scoring still costs $0.15–$0.80 per rejected candidate.
I would also ship smarter input (PR-D) before winner loop (PR-C). Editors wanted URL→brief from day one; the winner loop was useless without fast brief entry.
The pattern: generate many, rank fast, iterate winners
Hook Machine treats hooks as a search problem, not a generation problem. Generate many cheap candidates, score and rank instantly, let editors iterate on winners. Content-aware routing and pre-spend guards keep the search affordable; karaoke captions and seed frame dispatch keep the winners quality-competitive with manually edited hooks.