Hook Variations logo

Hook Variations

Community
mrpaidsocial
hook-variations

Combine ONE finished body cut with MANY hook clips to produce one standalone video per hook — the hook A/B batch you upload as separate ad creatives. Use whenever someone wants to "add all these hooks to this body", "combine this video with every hook in that folder", "make 21 versions with different openers", "swap the hook", "build hook variants", "test these hooks against the same body", or hands over one body file plus a folder of hooks and expects N finished videos out. Also use for the same shape with different words: intro variants, opener tests, cold-open A/B sets, or re-running an existing variant batch against a NEW cut of the body. Joins losslessly (body frames stay bit-identical), matches hook loudness to the body so the seam does not jump, and verifies with AVFoundation because an ffmpeg-only check cannot catch the parameter-set freeze that makes these files play fine in ffmpeg and stall in QuickTime. Do NOT use for editing a single video, adding captions, or cutting the hooks themselves out of a longer recording.

Overview

Publishermrpaidsocial
Repositorymrpaidsocial-video-editor-agent
Skill namehook-variations
Stars
114
Forks
34
Bundled files
7
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 7 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by mrpaidsocial on GitHub. Read the source before you install it.

Installation

Install the Hook Variations AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
    https://github.com/mrpaidsocial/mrpaidsocial-video-editor-agent/tree/main/.claude/skills/hook-variations
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/mrpaidsocial/mrpaidsocial-video-editor-agent.git /tmp/mrpaidsocial-video-editor-agent
mkdir -p .claude/skills
cp -r /tmp/mrpaidsocial-video-editor-agent/.claude/skills/hook-variations .claude/skills/hook-variations
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Hook Variations in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Hook Variations on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Hook Variations is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

hook-variations — one body × N hooks

You have one finished body cut and a folder of hooks. You are producing one standalone video per hook, each good enough to upload as its own ad creative.

The join itself is four lines of ffmpeg. Everything in this skill exists because the obvious four lines produce files that look correct in every tool you would naturally reach for and are broken on the user's disk. A hook batch has been delivered broken before, after passing a thorough QA pass, because the build and the QA used the same decoder.

Shape of the deliverable

hook → body, hook first. The hook is the scroll-stopper; the body is the payload that does not change between variants.

Before you build, read the body's first few seconds. Bodies often open with their own bridge line ("I'm about to show you the easiest way…"). That is usually a transition into the payload, not a competing hook, and it should stay — but it is the user's call, so say which way you read it and offer the alternative in one line rather than silently trimming. Never cut body content to make the join feel tighter without asking.

Do this in order

1. Pre-flight — always, it takes ten seconds

bash
python3 scripts/probe_join.py --body <cut.mp4> --hooks <hooks/>

This prints, per hook: geometry, timebase, audio-tail overrun, whether its avcC matches the body, and its loudness delta — plus what the build will have to do about each. Read it before building. Every trap below shows up in that table, and it tells you whether the hooks can be stream-copied (free, lossless) or need a re-encode.

If geometry differs (resolution, fps, pix_fmt), stop: these are not drop-in hooks and scaling them is a separate decision to raise with the user.

2. Build

bash
python3 scripts/build_variants.py --body <cut.mp4> --hooks <hooks/> --out <variants/> \
  --preset slow --crf 18            # match how the BODY was encoded

--preset/--crf matter only for hooks that need re-encoding, and they must match the body's own encode. Find them in the body's build script or ask; guessing produces a parameter-set mismatch, which the build refuses to mux (see trap 3).

Writes the variants plus variants.json (the manifest every later step reads).

3. Verify — the step that is not optional

bash
swiftc -O scripts/avtest.swift -o scripts/avtest     # once per machine
python3 scripts/verify_variants.py --body <cut.mp4> --hooks <hooks/> --out <variants/>

Do not report success off the build's own "OK" lines. See Why ffmpeg cannot sign this off below — this is the single most important paragraph in the skill.

4. Name them

Route through the naming-convention skill. Variant batches are unusable when every file is ad-v5-hook-07.mp4; the filename has to carry what differs. That skill also covers the trap of inheriting a wrong subject label from an upstream cut list.

5. Deliver

Route through video-review-canvas, and lead the reply with the URL. For a batch, publish 720p proxies rather than the masters — a set of 21 two-minute 1080×1920 masters is well over a gigabyte and too heavy to publish, while the canvas exists to judge the cut and the seam.

bash
python3 scripts/build_review.py --out <variants/> --version v1 --map <subjects.json>

--map is the naming-convention subjects file, which gives each card a real title ("Lookalike sunglasses · Lego-style ad") instead of "Hook 20". Each card also prints the master's filename, so canvas ↔ disk is a direct match when a reviewer says "number 7 is the one". Proxies that already exist are skipped, so re-running after a rename costs seconds. Bump --version every round: it stamps the proxy filenames, and without that the browser serves the previous cut from cache and you get "why isn't the new one there?".


The four traps

Three of these fail silently. That is what makes them worth knowing rather than rediscovering.

1. Timebase — the concat demuxer does not rescale the second file

It takes its output timebase from the first file. Hooks at 1/15360 in front of a body at 1/30000 stretch the body's timestamps by 1.95× — a 121-second file that reports 236s and 15.36fps. No warning. Fixed by remuxing both to a common timescale.

2. Audio overrun — the offset comes from the container, not the video

AAC frames are 1024 samples, which never lands on a 30fps boundary, so a hook's audio routinely runs tens of milliseconds past its last video frame. The demuxer offsets the next file by the container duration, so the body starts off the frame grid and stays lip-sync drifted for its whole length. Doing the video pass video-only fixes this at the root: a video-only container's duration is its video duration.

Also pin duration in the concat list to the hook's exact frames/fps. Some mp4 muxers write a track duration derived from a millisecond-rounded value (e.g. 801990 ticks instead of 802000), which starts the body a third of a frame early. That directive is only trustworthy once both files share a timebase — with a mismatch it compounds the offset instead of setting it, which makes it look like a dead end if you try it too early.

3. Parameter sets — the one that ships broken

If the hook and body were encoded with different settings, they carry different SPS and PPS. An mp4 avc1 track holds exactly one avcC, and concat writes the first file's.

ffmpeg plays the result perfectly, because it honours the in-band SPS/PPS the demuxer inserts at each IDR. AVFoundation — QuickTime, Finder, Photos, most upload validators — ignores in-band parameter sets in an avc1 track and reads only avcC. So the hook plays and the picture freezes on the body's first frame while audio runs on.

The fix is to make both halves share one parameter set. Re-encode the hook, not the body: the hook is short and the body is the long, approved, quality-critical half. Matching the body's preset/CRF/pix_fmt gets the PPS and level to agree; setsar=1 is usually the last byte, because a body that signals square pixels explicitly (aspect_ratio_info_present_flag=1) will not match a plain re-encode that omits it.

There is a tell you will probably see before the freeze: framemd5 over -c copy reporting exactly the keyframes as changed. That is the demuxer inserting parameter sets mid-stream. It is easy to read as "correct and required" and move on — it is in fact the symptom. If two halves need different parameter sets inserted at a boundary, one avcC cannot describe both. Go read avcC.

4. Loudness — hooks are usually much quieter than the body

Hooks cut straight from a raw mix commonly sit 10–20 LU below a body that has been through a loudnorm pass. Joined raw, every variant is a hook you can barely hear followed by the body arriving at full level. Match each hook to the body's integrated LUFS.

Two things about doing that:

  • loudnorm:linear=true cannot do it when the lift is large. A +15 dB lift would put peaks above 0 dBTP, so linear mode caps the gain to protect the ceiling and lands several LU short. Static gain then a limiter reaches the target; the limiter only touches the loudest transients.
  • alimiter limits sample peaks, not true peaks. Inter-sample peaks overshoot it, so a ceiling of −1.0 dBFS has shipped files at −0.1 dBTP, hot enough to clip in a platform transcode. The default here is 0.79 (−2.05 dBFS), which lands around −1.1 to −1.6 dBTP.

Why ffmpeg cannot sign this off

A set of variants once passed all of this: every frame decoded, pixel-identical to source, constant 30fps, zero audio lag, loudness verified. And every file froze after the hook in QuickTime.

The build used libavcodec. The check used libavcodec. libavcodec is the most permissive decoder in the chain, and it papers over exactly the defect that was present. A check that shares a blind spot with the thing it checks proves nothing — the failure was found by the user pressing play, which is the worst possible QA loop.

So verify_variants.py shells out to avtest, a small Swift AVAssetImageGenerator probe, and decodes 12 points across every file with AVFoundation. It also asserts the file's avcC equals the body's. Those two checks are the only ones that would have caught it. Build avtest once per machine with swiftc -O scripts/avtest.swift -o scripts/avtest; if it is missing, the verifier reports the probe as a FAILURE rather than quietly skipping it.

The generalisation is worth carrying beyond this skill, and it is the repo's own rule in a sharper form: verify in the medium the user will consume, with a different tool than you built with.

Two more things that will bite

Workers do not inherit module globals. ProcessPoolExecutor uses the spawn start method on macOS, so each worker re-imports the module with its own argv — CLI flags are gone there. A verifier that read its paths from globals had all five workers fall back to defaults and check a different folder while the parent printed the labels you asked for: 21 confident "ALL PASS" lines about files nothing had opened. Caught only because a frame count came back 3 short. verify_variants.py passes config into the worker as arguments and prints the folder and body under test at the top of every run — if that header does not name what you meant, stop.

Properties inherited from the body are not yours to fix silently. A body whose audio ends before its picture (or vice versa) will hand every variant the same quirk. Pad to hold sync, then say so — e.g. "the last 2 frames are silent, that comes from the body, want the tail trimmed?" Do not quietly alter the body's own timing to tidy it up.

Re-running against a new cut of the body

Common: the body gets a revision and the whole batch is rebuilt. Everything is parameterised, so it is the same commands with a different --body and --out. Keep each body's variants in its own folder and keep the old set unless told otherwise — these are usually net-new creatives, not replacements.

Do not assume a new body cut matches the old one's encode. probe_join.py on the new body answers it in seconds, and the answer decides whether hooks can be copied or must be re-encoded.

Files

PathWhat it is
scripts/probe_join.pypre-flight compatibility table: geometry, timebase, audio tail, avcC, loudness
scripts/build_variants.pythe build; copies hooks that already match, re-encodes those that do not
scripts/verify_variants.pyfull verification incl. the AVFoundation probe and avcC assertion
scripts/avtest.swift30-line AVFoundation decode probe (swiftc -O it once per machine)
scripts/build_review.py21-up gallery review canvas from variants.json
assets/canvas_template.htmlthe canvas page (per-variant comment box, timeline stamping)
assets/data.jsonhere.now Site Data manifest that turns on the per-variant comment store

Related skills: naming-convention (name the batch), video-review-canvas (deliver it), edl-tighten (if the hooks themselves still need cutting out of a longer recording).

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Hook Variations AI skill do?

Combine ONE finished body cut with MANY hook clips to produce one standalone video per hook — the hook A/B batch you upload as separate ad creatives. Use whenever someone wants to "add all these hooks to this body", "combine this video with every hook in that folder", "make 21 versions with different openers", "swap the hook", "build hook variants", "test these hooks against the same body", or hands over one body file plus a folder of hooks and expects N finished videos out. Also use for the same shape with different words: intro variants, opener tests, cold-open A/B sets, or re-running an...

Why use Hook Variations on TypingMind?

Because you install it once and use it with any model. Hook Variations is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Hook Variations in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/mrpaidsocial/mrpaidsocial-video-editor-agent/tree/main/.claude/skills/hook-variations. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Hook Variations?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Hook Variations?

As many as you like. As long as a model supports skills, you can use Hook Variations with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Hook Variations AI skill free?

It is published on GitHub by mrpaidsocial. Check the repository for licensing terms. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇