create-post-clips

Turn an existing long-form video (a YouTube link, a local file, or one from your Marky library — webinar, podcast episode, talk, sermon) into 2-5 short vertical clips: find the strongest 30-60 second

SKILL.md

GitHub

Create Post: Clips

Most businesses already have hours of long-form video doing nothing — webinars, podcast episodes, talks. The short-form versions are the highest-reach posts they never make. This skill mines a long video for its best moments and ships them as scheduled posts.

Pipeline shape and hard-won ffmpeg/Whisper details adapted from ericosiu/ai-marketing-skills (MIT).

Read the marky-api skill first for auth, endpoints, and the brand cache. Base URL https://api.mymarky.ai/api, header Authorization: Bearer mk_live_YOUR_KEY.

Prerequisites

command -v yt-dlp ffmpeg whisper
  • yt-dlp and ffmpeg are required. If missing, ask the user before installing, then offer: brew install yt-dlp ffmpeg (macOS) or their package manager's equivalent.
  • whisper (OpenAI's CLI, pip install -U openai-whisper) gives word-level timestamps for tight caption sync. It is optional for YouTube sources — auto-captions work as a transcript fallback — but burned-in captions look best with it. Offer the install; if the user declines, continue with the fallback and say captions will be line-level.

Workspace: put everything in ~/.marky/clips/<business_id>/<slug>/ — not the current working directory. Video work is big and cwd may be inside someone's repo. Delete the source download when the run is done.

Stage 1 — Get the source video

  • YouTube link (also grabs auto-captions as the transcript fallback):

    yt-dlp \
      --write-auto-sub --sub-lang en --convert-subs vtt \
      -f "bestvideo[height<=1080]+bestaudio/best[height<=1080]" \
      --merge-output-format mp4 \
      -o "source/%(title)s.%(ext)s" \
      "https://www.youtube.com/watch?v=VIDEO_ID"
    
  • Local file: copy or symlink it into the workspace.

  • Marky library: search_media_library (or GET /businesses/BIZ_ID/media) to find it, then download the returned URL.

Check the duration with ffprobe. Under ~10 minutes there may only be 1-2 good clips; tell the user and lower the target instead of forcing five.

Stage 2 — Transcribe with timestamps

Preferred — Whisper on the source (word timestamps power everything downstream):

whisper source/video.mp4 --model small --language en \
  --word_timestamps True --output_format json --output_dir transcript/

Fallback — parse the downloaded .vtt auto-captions into [MM:SS] line entries. Dedupe repeated lines (YouTube VTTs overlap heavily).

Stage 3 — Find the best moments (you are the segmenter)

Read the timestamped transcript and pick the 2-5 strongest 30-60s segments. Criteria:

  • Hook in the first 3 seconds — the opening line must grab: surprising stat, bold claim, counterintuitive take, or a story that starts mid-action. Score each candidate's hook 1-10; only keep clips scoring 7 or higher. The clip must OPEN on the hook line — never on setup.
  • One complete idea — one framework, one stat, one story. Not a summary.
  • Emotional punch — surprise, humor, a strong opinion, or data that shocks.
  • Ends on a takeaway or cliffhanger.
  • Ideal 30-45s, hard max 60s. Skip anything under 30s.
  • Avoid: segments starting mid-sentence, meandering setup, references to earlier content ("as I mentioned..."), multiple unrelated ideas.

For each clip record: start_time, end_time, title, hook_sentence (exact first line), payoff_sentence, hook pattern (stat / contrarian / story / question / bold claim — see plan-social-content's hook palette), and a layout guess: talking_head (one presenter), screen_share (slides/screen with a webcam bubble), or side_by_side.

Stage 4 — Verify every cut (do not skip)

Second pass, one clip at a time: re-read the transcript ±10 seconds around each proposed end time and ask: does the thought actually complete there? A clip must end on a complete thought or reaction — never mid-sentence, never mid-idea. If it runs on, move end_time to where the thought resolves. This pass catches the single most common failure (a clip that cuts off the punchline) and is cheap. If a corrected clip now exceeds 90s, trim the end back to about 75s at the nearest sentence boundary instead.

Show the user the clip list (timestamps, titles, hooks) before cutting — a 10-second sanity check here beats re-rendering later.

Stage 5 — Cut and crop to 9:16

Cut each clip (re-encode; stream-copy lands on the wrong keyframe):

ffmpeg -y -ss START_SECONDS -i source/video.mp4 -t DURATION_SECONDS \
  -c:v libx264 -c:a aac -avoid_negative_ts make_zero clips/clip-1.mp4

Look before you crop. Extract a frame and view it to confirm the layout guess and where the speaker sits:

ffmpeg -y -ss 2 -i clips/clip-1.mp4 -frames:v 1 clips/clip-1-frame.png
  • Talking head — center crop to 9:16, then scale. Crop BEFORE scaling. If the speaker is off-center, shift the crop x-offset toward them (read the frame you extracted):

    ffmpeg -y -i clips/clip-1.mp4 \
      -vf "crop=ih*9/16:ih,scale=1080:1920" \
      -c:v libx264 -preset medium -crf 20 -c:a copy clips/clip-1-v.mp4
    
  • Screen share / side by side — do not center-crop (you lose the content). Stack: screen region on top, speaker region below. Take the two regions from the frame you extracted and adjust the crop coordinates to match:

    ffmpeg -y -i clips/clip-1.mp4 -filter_complex \
      "[0:v]crop=iw:ih*3/4:0:0,scale=1080:1150[top];\
       [0:v]crop=ih*3/4:ih*3/4:iw-ih*3/4:ih/4,scale=1080:770[face];\
       [top][face]vstack=inputs=2[v]" \
      -map "[v]" -map 0:a -c:v libx264 -crf 20 -c:a copy clips/clip-1-v.mp4
    
  • Anything unclear — safe fallback is letterbox: full frame scaled onto a 1080x1920 canvas with black bars:

    ffmpeg -y -i clips/clip-1.mp4 \
      -vf "scale=1080:-2,pad=1080:1920:(ow-iw)/2:(oh-ih)/2:black" \
      -c:v libx264 -c:a copy clips/clip-1-v.mp4
    

Verify each output is 1080x1920 before moving on:

ffprobe -v error -show_entries stream=width,height -of csv=p=0 clips/clip-1-v.mp4

Stage 6 — Burn in captions

Silent-scroll viewers are most of the audience; clips without captions underperform. Re-run Whisper on each CUT clip — never reuse source-video timestamps, the offsets drift and captions desync:

whisper clips/clip-1-v.mp4 --model small --language en \
  --word_timestamps True --output_format srt --output_dir clips/

Two burn-in paths:

  • Path A — HyperFrames. If the hyperframes skill (and its embedded-captions workflow) is available in your session, use it — it produces the word-highlight style natively and handles brand fonts/colors. Hand it the cut vertical clip.

  • Path B — ffmpeg + ASS. First confirm the local ffmpeg was built with libass (some minimal builds are not):

    ffmpeg -hide_banner -filters 2>/dev/null | grep subtitles
    

    Then write an .ass file from the SRT (3-4 words per Dialogue line, timed from the word timestamps) with this style block, and burn:

    [Script Info]
    PlayResX: 1080
    PlayResY: 1920
    WrapStyle: 0
    
    [V4+ Styles]
    Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
    Style: Default,Arial Black,64,&H00FFFFFF,&H0000FFFF,&H00000000,&H80000000,-1,0,0,0,100,100,0,0,1,4,0,2,40,40,270,1
    
    [Events]
    Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
    Dialogue: 0,0:00:00.00,0:00:01.20,Default,,0,0,0,,THE HOOK WORDS
    
    ffmpeg -y -i clips/clip-1-v.mp4 -vf "subtitles=clips/clip-1.ass" \
      -c:v libx264 -crf 20 -c:a copy clips/clip-1-final.mp4
    

Colors: ASS uses &HAABBGGRR (blue-green-red, not RGB). Swap the highlight color for a brand accent from the brand cache if it stays readable on video.

Stage 7 — Upload and draft one post per clip

For each final clip:

  1. Upload:

    curl -X POST "https://api.mymarky.ai/api/businesses/BIZ_ID/media" \
      -H "Authorization: Bearer mk_live_YOUR_KEY" \
      -F "file=@clips/clip-1-final.mp4"
    

    Keep the returned URL. Uploads cap at 50 MB; if over, raise -crf to 23 and re-encode.

  2. Write the caption in the brand voice. Start from the clip's hook_sentence — it already earned its place — then rewrite per the brand profile (tone, caption_writing_rules, caption_suffix; see "Write like the business" in the marky-api skill). Run every caption through the humanize skill's pattern check (score 90+, fix silently) before showing the user.

  3. Create the post as a draft with metadata: {"media_type": "video", "format": "clip", "hook": "<pattern>", "topic": "...", "created_by": "create-post-clips"} — per "Tag every post" in the marky-api skill.

Stage 8 — Approve, then schedule (hard gate)

  1. Present all clips on the review board in approve mode — exactly the plan-social-content Stage 6 flow: write items.json (one item per clip, media_url pointing at the local final file or uploaded URL, meta like "Clip 2 - 0:42 - stat hook"), run ${CLAUDE_PLUGIN_ROOT}/scripts/review-board.py items.json in the background, parse BOARD_URL:, AskUserQuestion with the URL, then read feedback.json. Apply edits verbatim; append the feedback to ~/.marky/feedback-log.jsonl with context: "clips".
  2. Nothing is scheduled until the user approves. Rejected clips are dropped; edited captions use the user's wording.
  3. Schedule approved clips via the schedule-posts skill, spaced across the week (for example Tue/Thu/Sat at the same hour) — never all on one day. Video is supported everywhere: target every connected platform whose integration status is valid.
  4. After the scheduled times pass, check publish_results per post shows success.

Troubleshooting

  • FFmpeg filter_complex error: don't use -c:v copy with -filter_complex. Only -c:a copy is safe.
  • Wrong output resolution: always crop before scaling. Verify with ffprobe -show_entries stream=width,height.
  • Caption sync issues: run Whisper on the cut clip, not the source episode.
  • TikTok upload fails: ensure H.264 + AAC encoding. Add -c:v libx264 -c:a aac.
  • Clip too long: segmentation sometimes overshoots. Auto-trim anything over 90s back to ~75s at a sentence boundary.
  • "Unknown filter 'subtitles'": that ffmpeg was built without libass. Use Path A (HyperFrames) for the caption burn, or install a full ffmpeg build.
  • No auto-captions on the YouTube video: Whisper becomes required; install it or stop.

Marky wants your feedback. If anything breaks or is confusing while you run this skill — and again once the clips are scheduled — send Marky a quick note with the submit_feedback MCP tool. See "Marky wants your feedback" in the marky-api skill.