Install
$ agentstack add skill-boshify-ai-seo-agent-skills-video-screenshot-extractor ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Video Screenshot Extractor
What This Skill Does
Takes a video URL and an existing blog post, identifies every section in the post that references a specific visual artifact, extracts the matching clean frame from the video at full quality, writes SEO-optimized EXIF metadata, and hands back a modified blog post with each screenshot wrapped in a timestamp-deeplinked link back to the exact second of the video.
Context the Agent Needs
This skill is a pipeline step, not a content generator. The blog post has already been written (typically by transcript-to-blog-post). The skill does not decide what the post should say or whether it needs images — it identifies where the existing prose is making a visual claim and supplies the matching proof frame.
Why clean frames matter. Screenshots that contain pause icons, progress bars, or player overlays look like screen-grabs from a YouTube tab, which tells readers (and Google) that the image is a lazy capture rather than a first-party asset. Extracting directly from the downloaded video file with ffmpeg gives pure frames with zero player chrome.
Why timestamp deeplinks matter. Each screenshot is wrapped in a link of the form https://youtube.com/watch?v=VIDEO_ID&t=145s. A reader who clicks the image lands on the exact moment of the video where the claim is demonstrated. This is the killer feature — it turns static images into proof checkpoints, signals the video as the source document to search engines, and dramatically improves dwell time.
Portability is a hard constraint. The skill runs as shell commands on whatever machine executes it — OpenClaw, Claude Code, a local terminal, CI — with no assumptions about OS, container, or path layout. Every path is relative to --work-dir. Every dependency is verified or installed at Step 0. Never hard-code /tmp, /data, /mnt/user-data, or any environment-specific path.
Frame matching uses the transcript when available. Jonathan's proof videos are screen recordings of dashboards, SERPs, and page layouts with narration. The video transcript (pulled via yt-dlp --write-auto-subs) tells us when the narrator references each artifact, and scene-change detection tells us where the visual content actually changed. Intersecting those two signals produces the best frame per section. User-provided timestamps override everything.
Workflow — Execute In This Order
Do not skip Step 0. Do not assume dependencies are present. Do not overwrite the source blog file.
STEP 0: Environment Preflight
Verify dependencies and install what can be installed without system privileges.
Input: None.
Process:
- Run
bash scripts/preflight.sh. The script checks forffmpeg,yt-dlp,exiftool, and the Python librariesPillowandpiexif. It auto-installsyt-dlp,Pillow, andpiexifviapip install --user. It never tries to installffmpeg— that requires platform-specific instructions. - If
ffmpegis missing, stop with a clear install instruction per OS (brew install ffmpeg/apt-get install ffmpeg/choco install ffmpeg). Do not attempt to proceed without it. - If
exiftoolis missing, continue silently — the skill falls back topiexif+Pillowfor EXIF writing.
Output: All required dependencies confirmed available.
Decision gate:
- If
ffmpegabsent → stop and surface install instructions. - If
yt-dlp/Pillow/piexifinstallation fails → stop and report the pip error. - If
exiftoolabsent → continue (fallback path is fully supported).
STEP 1: Normalize Inputs
Resolve the video URL and the blog post into consistent internal formats.
Input: --video , --blog , optional --timestamps, --keyword, --creator, --work-dir.
Process:
- Resolve
--work-dir. If not provided, use the current working directory. Create./screenshots/relative to it. - Normalize the blog post to markdown text:
- If the input ends in
.md, read it as-is. - If the input ends in
.htmlor.htm, convert to markdown viapandocif available, otherwise strip tags with a minimal HTML-to-markdown conversion. - If the input is a URL, fetch it with the
page-scraperskill or a simplecurl+ HTML-to-markdown conversion.
- Capture the blog filename stem for the output file. A blog at
./pillar-post.mdproduces./pillar-post.with-images.md. - Parse the video URL. Extract the canonical video ID for timestamp-deeplink construction. For YouTube:
v=VIDEO_ID. For Vimeo and others: preserve the URL and append#t=145ssince&t=isn't universally supported. - If
--timestampsprovided, validate each as HH:MM:SS or MM:SS and convert to seconds.
Output: Clean markdown blog text, canonical video URL, video ID, parsed timestamp list (or null), resolved work-dir paths.
Decision gate:
- If the blog input cannot be resolved (404, unreadable file, binary) → stop and report.
- If the video URL is clearly invalid → stop.
STEP 2: Identify Visual-Artifact Sections
Read the blog and list every section that makes a specific visual claim the screenshot should prove.
Input: Normalized blog markdown.
Process:
- Walk the blog's heading tree (H2, H3, H4) and the prose under each.
- For each section, ask: does this section reference a specific visual artifact that the reader is expected to see? Examples of what qualifies:
- "The Ahrefs chart for Edia shows a 40% traffic collapse"
- "Here's the SERP for [keyword]"
- "The SEMrush dashboard looks like this"
- "This is the layout of [page]"
- Language cues: "as you can see", "looks like this", "the screenshot below", "here's the chart", "the dashboard shows", embedded image placeholders like `` with no src filled in
- Skip sections that are purely narrative, opinion, FAQ, or conceptual. A section titled "Why Mass AI Content Strategies Collapse" that opens with prose and then injects the Edia case study H3 gets one screenshot (the Edia chart) — not one for the parent H2.
- For each qualifying section, record: section heading, the sentence/phrase that anchors the visual claim, and 3-5 transcript-search terms likely to match the moment in the video narration.
- Respect the hard cap. If more than 10 visual-artifact sections exist, stop and surface the list to the user for them to pick the top 10.
Output: Ordered list of target sections, each with a heading, anchor phrase, and transcript search terms.
Decision gate:
- If zero visual-artifact sections → stop and report. The blog doesn't call for screenshots.
- If > 10 sections → stop and ask the user to pick 10.
- If 1-10 → proceed.
STEP 3: Download the Video and Transcript
Pull the video file and its auto-captions in one pass.
Input: Canonical video URL.
Process:
- Run
python3 scripts/download.py --url --out /video.mp4. This wrapsyt-dlpwith flags for the best-available MP4 (no re-encoding) and also requests auto-generated captions in VTT format. - The script produces
/video.mp4and/video.vtt(if captions available). - If captions are unavailable (creator disabled them, or Vimeo/direct MP4 has no transcript), continue with the VTT file absent. The pipeline degrades gracefully — Step 5 will fall back to proximity-only matching.
Output: Local video file path, optional transcript VTT path.
Decision gate:
- If download fails due to geo-block, privacy, or removal → stop and report.
- If transcript is unavailable → log a warning and continue.
STEP 4: Build the Candidate Frame Pool
Produce a list of candidate timestamps that represent distinct visual moments.
Input: Local video file, optional --timestamps override.
Process:
- If
--timestampswas provided, this is the candidate pool. Skip detection entirely and move to Step 5 with one candidate per provided timestamp. - Otherwise, run
python3 scripts/scenes.py --video --threshold 0.3. The script runsffmpeg -vf select='gt(scene,0.3)'and prints a list of timestamps where the visual content changed meaningfully. For a 10-minute screen recording this typically yields 30-80 candidates. scenes.pyautomatically retries at lower thresholds (0.2, then 0.15) if the initial pass yields fewer than--min-candidates(default 5). This catches softer transitions — fades, slow zooms, cross-dissolves — that a 0.3 threshold misses. The script writes the threshold actually used to stderr as# threshold_used=. If you observe that the final matched frames keep landing in large no-change gaps, re-run with--threshold 0.2explicitly.- If scene detection still returns fewer than 3 candidates after the retry chain (rare — typically talking-head videos with a locked camera), fall back to evenly-spaced samples: one frame every
video_duration / (N + 1)seconds, where N is the number of target sections from Step 2.
Output: Array of candidate timestamps in seconds.
Decision gate:
- If user-provided timestamps exist → use them verbatim, no detection.
- If scene detection produces --ts --out /screenshots/.png`.
- The script invokes
ffmpeg -ss -i -frames:v 1 -c:v png -compression_level 0 -q:v 1— fast seek, single frame, lossless PNG, maximum quality. - Filenames are kebab-case, descriptive, SEO-friendly: derived from the section slug with a two-digit zero-padded index. Example:
edia-ahrefs-chart-01.png,serp-page-layout-02.png. - Frames come from the raw video stream. They contain no pause icons, no progress bars, no player UI. Verify this visually by opening one before continuing.
Output: Full-quality PNG files in /screenshots/.
Decision gate:
- If any ffmpeg call fails (unreadable timestamp, codec mismatch) → retry once with a 500ms offset; if still failing, skip that section and note it in the manifest.
STEP 7: Generate SEO Assets per Screenshot
For each image, produce the alt text, optional caption, filename, and EXIF payload.
Input: Matched sections + extracted frames + blog keyword.
Process:
- Determine the primary target keyword. If
--keywordwas provided, use it. Otherwise, infer from the blog's H1, meta-title, and the most-repeated 2-3 word phrase excluding stop words. - For each screenshot, write:
- Alt text (always): a descriptive sentence of what the image shows, with the target keyword or a natural variation included — never stuffed. Example:
Ahrefs organic traffic graph showing Edia's 42% decline from 2023 to 2024. - Caption (conditional): only if the surrounding prose doesn't already describe what's in the image. Skip otherwise. Redundant captions hurt readability.
- Filename (derived from section slug): already generated in Step 6.
- EXIF Title: matches the filename without the extension, human-readable. Example:
Edia Ahrefs Chart. - EXIF Description: one sentence, reuses or refines the alt text for the context of the blog topic.
- EXIF Keywords: target keyword + 2-3 natural variations, comma-separated.
- EXIF Artist/Creator: from
--creatorflag, or inferred from blog author metadata, or empty. - EXIF Copyright:
if creator present, else blank. - EXIF Source: the canonical YouTube URL (without
&t=parameter). Keep it clean.
Output: A dict per screenshot containing filename, alt, caption-or-null, and the EXIF payload.
Decision gate:
- If alt text would repeat the target keyword more than once → rewrite with a natural variation.
STEP 8: Write EXIF Metadata
Stamp each PNG with the metadata from Step 7 and strip anything that fingerprints automation.
Input: PNGs from Step 6 + EXIF payloads from Step 7.
Process:
- For each PNG, run
python3 scripts/exif.py --file --title --description --keywords --artist --copyright --source. - The script prefers
exiftoolif available — it writes all fields across EXIF, XMP, and IPTC so any downstream reader finds the value regardless of which schema it prefers. - When
exiftoolis absent, the fallback path writes the EXIF-compatible fields (Title, Artist, Copyright) to the PNGeXIfchunk viapiexif+Pillow, AND writes the remaining SEO-critical fields (Description, Keywords, Source, Author) as PNGtEXtchunks. The tEXt chunks are a superset of what piexif alone produces and are readable by exiftool, ImageMagick, and most metadata-aware crawlers when the files are later re-ingested. - The script always strips: GPS data, camera make/model, software identifiers (
yt-dlp,ffmpeg,Pillow), and any pre-existing EXIF from the source video's frame. - The script never writes:
Comments,UserComment, or URL parameters in the Source field.
Output: PNGs with clean, SEO-optimized EXIF.
Decision gate:
- If neither
exiftoolnorpiexif+Pillowcan write metadata (extremely rare on modern systems) → produce the images without EXIF and flag it in the manifest for manual post-processing.
STEP 9: Inject Images into the Blog + Write Manifest
Produce the two output markdown files.
Input: Target sections, matched frames, EXIF payloads, original blog markdown, video ID.
Process:
- For each target section, compose the image block using the wrapped-link pattern:
``markdown []() ``
- `
ishttps://youtube.com/watch?v=VIDEO_ID&t=sfor YouTube, or the canonical URL with#t=s` fragment for other sources. - If a caption was generated in Step 7, append it on the line below as
*Caption text here.*(italic markdown).
- Insert each image block into the blog at the end of the first paragraph of its section. This keeps the visual close to the claim it proves without disrupting prose flow.
- Write the modified blog to
/.with-images.md. The original file is never touched. - Write
/screenshots/manifest.md— see Output Format below.
Output:
/.with-images.md— modified post ready for review and CMS publishing./screenshots/manifest.md— per-image review doc.
Decision gate:
- After writing, spot-check that every image file referenced in the modified blog exists in
./screenshots/. If any are missing, the pipeline has a bug — stop and surface it.
Output Format
manifest.md
# Screenshot Manifest
Video:
Blog post:
Total screenshots:
---
## .png
- **Section:**
- **Timestamp:** (s)
- **Alt text:**
- **Caption:**
- **Deeplink:**
- **Reasoning:**
- **Confidence:** high | medium | low
---
.with-images.md
Same content as the original blog, with image blocks inserted after the first paragraph of each target section:
## Why Edia's Content Strategy Collapsed
Edia grew from 50k to 2M monthly visits in 18 months. The crash, when it came, was just as fast.
[](https://youtube.com/watch?v=abc123&t=163s)
*Edia's Ahrefs organic traffic timeline, showing the 42% collapse between March and September 2024.*
The pattern is visible across every site that tried to scale with pure AI-generated content...
Formatting rules:
- Image block is a wrapped link:
[](url-with-timestamp). Always. - Path is relative (
screenshots/filename.png), never absolute. - Timestamp deeplink uses
&t=sfor YouTube,#t=sotherwise. - Caption italicized on its own line, only if present.
- Single blank line before and after the image block.
Edge Cases & Judgment Calls
When the blog references zero visual artifacts: Stop and report. The skill doesn't exist to sprinkle decorative images into prose — it exists to supply proof frames for specific claims. If the blog doesn't make visual claims, the right action is to tell the user and let them decide whether to rewrite the post or skip the skill. Do not invent image spots.
When the blog references more than 10 visual artifacts: Stop and surface the full list to the user with a prompt to pick their top 10. Ten is a deliberate cap — more screenshots past that point crowd the reader and dilute each
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: boshify
- Source: boshify/ai-seo-agent-skills
- License: MIT
- Homepage: https://www.skool.com/ai-seo/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.