跳到正文
DEV Community· Frank Heijdenrijk·· 3 小時前AI 評分66

Claude Code 的 YouTube 技能如何選擇搜尋詞、核對 YouTube 限制並為縮圖畫面評分

YouTube Skill for Claude Code: How It Picks the Search Phrase, Counts YouTube's Limits and Scores Thumbnail Frames

AI 導讀

Frank Heijdenrijk 拆解其 YouTube Agent 技能如何選擇搜尋詞、核對 YouTube 限制並為縮圖畫面評分。

正文

YouTube skill for Claude Code, Codex, Cursor and OpenClaw: a free, MIT-licensed agent skill that takes a finished video and builds everything the upload needs to be found. It transcribes the video, reads what people type into YouTube search, writes 10 titles, a description, chapters, tags and hashtags, checks every field against YouTube's limits, renders thumbnail options from the video's own frames, writes a caption file, and uploads only after you say yes. This post skips the pitch and walks through the parts that are code: what the scripts count, how they count it, and why the defaults are what they are.

Repo: github.com/social-media-skills/youtube-skill

A raw upload with a filename title next to the package the skill built: search-first title, thumbnail from the video, description opening and 7 chapters

The repo's worked example. The video is "Earth Water Budget Animation", courtesy NASA/JPL-Caltech. The title, description, chapters and thumbnails were written by an AI agent with the skill; no endorsement by NASA, JPL or Caltech is implied.

Six scripts that count, one agent that chooses

The skill is a SKILL.md workflow plus six small Python scripts. The scripts handle everything with a right answer: byte counts, timestamps, ffmpeg calls, pixel work. The agent handles everything that needs reading: which search phrase the video actually answers, which title is honest, which frame shows what the video promises. Neither does the other's job.

final.mp4
  -> check_setup.py           Python, ffmpeg, Pillow, transcription routes, OpenCV (optional)
  -> transcribe.py            work/transcript.json + transcript.txt
       --from captions.srt | GROQ_API_KEY | OPENAI_API_KEY | local faster-whisper
  -> suggest.py               pack/suggestions.md: what people type into YouTube search
  -> agent reads the whole transcript, picks one phrase, writes upload.json
  -> youtube_pack.py build    checks every field, snaps chapters, writes description.txt,
                              captions.srt, post.json, manifest.json, upload-pack.md
  -> youtube_pack.py frames   frames.jpg: 12 scored candidate frames
  -> agent writes thumbs.json (2 or 3 different ideas)
  -> thumbnail.py             thumb-a.jpg, thumb-b.jpg ... and preview.png at feed size
  -> you approve
  -> WoopSocial               upload session, posts_validate, posts_create

Apart from Pillow, the scripts use only the standard library and ffmpeg. No paid API is required anywhere in the chain.

Finding the phrase: autocomplete, used lightly

suggest.py asks YouTube's search suggestions about the topic and its question forms (how to, how does, why, what is, is, best, vs). With --deep it also tries the topic followed by every letter. It writes the suggestions in YouTube's order and says plainly that the order is not a search volume.

That endpoint has no official API and no published terms, so the script treats it with care: a handful of requests per video, results saved to suggestions.json, and a docstring telling the agent never to loop it over hundreds of topics. One portability detail: Python from python.org on a Mac often ships without a certificate store, so fetch() tries urllib first (with certifi when installed) and falls back to curl, which every current Mac, Windows and Linux machine has.

The example shows why a phrase beats a topic. The broad "earth water" returns noise, such as the chemical elements and water parks. "how much water is on earth" is YouTube's top suggestion for the topic, and the video answers exactly that question, so it became the main phrase. Other suggestions such as "how much drinkable water is there on earth" went into the description body and the tags, where they're true.

With your own free YOUTUBE_API_KEY, --top also lists the 10 videos ranking for the phrase with views per day. As of October 2026, search.list has its own quota bucket of 100 calls a day at 1 unit each, so one run costs 1 of your 100 daily searches.

Counting the way YouTube counts

The YouTube Data API and the Help Center don't always agree. The Help Center describes a 5,000-character description; the API docs say 5,000 bytes. youtube_pack.py uses the stricter one, which matters as soon as a description contains emoji or accented letters.

Field What the script measures Warns when
Title characters over 100 (hard), over 70 (usually cut), phrase missing, mostly capitals, < or >
Description UTF-8 bytes over 5,000 bytes, phrase not in the first 150 characters, < or >
Tags characters as the API counts them over 500, or a tag contains <, > or a comma
Hashtags count and characters a space or symbol breaks one, or more than 60 (all are then ignored)
Chapters seconds not at 0:00, fewer than 3, any under 10 s, any after the end
Made for kids presence missing; it's a legal setting the user must declare
Schedule ISO time no timezone, or in the past

The tag budget is the least obvious one. The API counts the whole list against 500 characters, commas included, and a tag with a space is wrapped in quotes that count too:

def tags_length(tags):
    return sum(len(t) + (2 if " " in t else 0) for t in tags) + max(0, len(tags) - 1)

The example's 8 tags come to 187 of 500, and its description to 739 bytes. Those numbers land in manifest.json so the agent can report them without guessing.

The phrase check is deliberately loose about grammar and strict about words. phrase_in() drops function words ("is", "on", "the"), then looks for the start of each remaining word, so "Earth's" satisfies "earth" and "waters" satisfies "water". The title check also renders where a phone line usually ends, about 40 characters, which upload-pack.md shows as "How Much Water Is on Earth? Only 0.01% I...". The 40 and 70 are rules of thumb; YouTube publishes no number, and the bundled references/youtube-seo.md says so next to every limit it does publish, with sources.

Chapters snapped to the sentence they fall in

A chapter start read off a timestamp can land a moment after the sentence begins, and then the chapter opens mid-word. The pack moves each start to the nearest sentence start within 3 seconds, minus a 0.3-second lead-in:

for t, name in chapters:
    s = secs(t)
    if sentences and s > 0:
        near = min(sentences, key=lambda x: abs(x["start"] - s))
        if abs(near["start"] - s) <= 3.0:
            s = max(0.0, near["start"] - 0.3)

In the example, the agent wrote 1:30, 1:59 and 3:05; the pack published 1:29, 1:58 and 3:04. Then it applies YouTube's rules, which are all or nothing: first chapter at 0:00 (if missing, it adds one called "Intro" and warns you to rename it), at least 3 chapters, each at least 10 seconds, none after the end. Break any of them and YouTube shows no chapters at all, so every break is a warning the agent has to fix before the pack goes to you.

Scoring frames for thumbnails

youtube_pack.py frames samples the video evenly at three times the number of candidates wanted (36 frames for the default 12), skipping the first and last 2 seconds. Each frame, scaled to 640 pixels wide, gets a score:

score = (min(sharp, 2500) / 2500) * 0.5 \
      + (min(contrast, 70) / 70) * 0.3 \
      + (min(hsv, 120) / 120) * 0.2
if mean < 25 or mean > 235:
    score *= 0.2          # black or blown-out frames
if faces:
    score += 0.15 + min(face_share * 2, 0.2)

Sharpness is the variance of an edge filter, contrast is the standard deviation of brightness, and the last term is colour saturation. Faces come from OpenCV's YuNet model, which ships in the repo, with the older Haar cascade as a fallback; without OpenCV, faces simply aren't scored.

A high score alone would return twelve versions of the same shot, so each candidate gets a 64-bit difference hash and is kept only if it differs from every picked frame by more than 6 bits. A fixed-camera video can look identical all the way through; then the script fills the remaining slots with the frames furthest apart in time. The result is frames.jpg, a numbered contact sheet with times and scores.

The scores rank; they don't choose. The agent looks at the sheet and picks frames that carry the video's promise. When it renders, thumbnail.py doesn't trust the exact timestamp either: it pulls six full-resolution frames across half a second around it and keeps the sharpest, so a moving shot doesn't produce a blurred thumbnail.

The example's three ideas are deliberately different, not three crops of one frame: Earth drained dry next to "All of it in one drop", a surfer with "96.5% is salt water", and the full 370-quintillion figure with no words at all. Different ideas are what make an A/B test result mean something.

4K when the source has it, under 2 MB always

YouTube now recommends 3840x2160 thumbnails. It allows 50 MB from a computer but only 2 MB from a phone. thumbnail.py renders at the source's own size up to 3840x2160 (never upscaling past the source) and keeps every file under 2 MB, so it uploads from either:

def save_jpeg(im, path):
    while True:
        for q in (92, 88, 84, 80, 76, 72):
            im.save(path, "JPEG", quality=q, optimize=True, progressive=True)
            if os.path.getsize(path) <= MAX_BYTES:
                return q, im.size, True
        # still too big: step down 2160 -> 1440 -> 1080 -> 720, then lower quality at 720

The example's source is 1080p, so its three thumbnails came out at 1920x1080 and quality 92, between about 150 and 190 KB each.

Layout numbers are written once for a 1280-wide frame and scaled. Two of them do real work: words smaller than 80 pixels at 1280 wide trigger a warning (they vanish in the small card), and a 300x140 zone in the bottom-right corner stays clear, because the duration badge covers it.

Judged at the size people see it

A 4K thumbnail viewed full screen tells you nothing. preview.png draws each option, with your title and a duration badge, as a desktop home-feed card (360 pixels wide) and as the small card used for "up next" and phone search (168 pixels wide), on YouTube's dark theme. The workflow tells the agent to reject an option when its words can't be read in the small card or its subject is unclear at that size.

Shorts: 9:16, or YouTube replaces it

A vertical or square video of 3 minutes or less is a Short, and the pack spots it from the probe (rotation metadata included, so a phone clip is measured the way it plays). For a Short it drops chapters and doesn't ask for #shorts, since YouTube defines a Short by length and shape.

Thumbnails change shape too. YouTube replaces a 16:9 custom thumbnail on a vertical video with an auto-generated frame, so thumbnail.py renders 9:16 up to 2160x3840, centres the words in the upper part of the frame (the bottom third sits under the title and buttons), and swaps the preview for Shorts-shelf cards. Custom Shorts thumbnails can only be added in Studio on a computer, and Test & Compare doesn't run on Shorts; the pack's checklist says both.

Captions from the same transcript

captions.srt comes from the transcript the rest of the package was written from: fillers out, at most 2 lines of 42 characters, and a replace map for words the transcriber got wrong (the example uses it to capitalise "Earth" throughout). The example's transcript came from the video's existing caption file through --from, so word timing is estimated, and the pack prints that as a note rather than hiding it.

Mac, Windows and Linux

The requirements are Python 3.8 or newer, any ffmpeg build and Pillow. The skill tells the agent to use py instead of python3 on Windows, check_setup.py prints the install command for the system it finds, and the fonts (Poppins ExtraBold for thumbnail words, Inter for the preview) are bundled so text renders the same everywhere. A CI workflow builds a pack and renders thumbnails on Ubuntu, Windows and macOS. OpenCV and the YouTube API key are optional extras; everything else works without them.

Uploading, and what stays in YouTube Studio

post.json is the post body for WoopSocial, a social media API and MCP server for AI agents. The agent creates an upload session with the byte size from manifest.json, sends the parts with upload_parts.py, fills in the two IDs, runs posts_validate, and shows you the title, the description's first lines, chapters, tags, made for kids, privacy and time before it calls posts_create. In a pre-release trial, the worked example's full YouTube post body passed posts_validate against a real connected channel; it was not posted.

The API sets the title, description, tags, category, made for kids, privacy and schedule. WoopSocial doesn't write any of that text and doesn't edit or crop the video; the skill builds the package locally. It also doesn't set the thumbnail, caption files, playlists, end screens or the AI-use label, so upload-pack.md ends with those steps for YouTube Studio. After 2 and 7 days the agent reads views back and asks you for click-through rate and average view duration from Studio, and it never estimates a number it wasn't given.

The free plan covers 2 social accounts, with everything from posting to analytics included, and setup for each agent is at woopsocial.com/agents. Disclosure: the skill is maintained by Frank Heijdenrijk and the team behind WoopSocial. Uploading is optional; without it the skill hands you the pack to upload yourself.

Try it

npx skills add social-media-skills/youtube-skill -g -a claude-code -y

OpenClaw: openclaw skills install @social-media-skills/youtube-skill. Codex, Cursor and other agents: run npx skills add social-media-skills/youtube-skill and pick your agent. You need an agent that can run ffmpeg on your machine.

Then hand it a finished video: "put final.mp4 on my channel and make it findable." Issues and pull requests are welcome on GitHub, and more free skills for social media work are at social-media-skills.com.

來源:DEV Community · dev.to