{"slug": "youtube-skill-for-claude-code-how-it-picks-the-search-phrase-counts-youtube-s", "title": "YouTube Skill for Claude Code: How It Picks the Search Phrase, Counts YouTube's Limits and Scores Thumbnail Frames", "summary": "A developer released a free, MIT-licensed YouTube skill for Claude Code, Codex, Cursor and OpenClaw that transcribes a finished video, pulls YouTube search suggestions to pick a target phrase, and generates titles, descriptions, chapters, tags and thumbnail candidates before uploading on approval. The skill pairs a SKILL.md workflow with six small Python scripts that handle byte counts, timestamps, ffmpeg and pixel work, while the agent handles phrase selection and title writing. The author notes the suggestion endpoint has no official API, so the script caps requests per video and falls back to curl when Python lacks a certificate store.", "body_md": "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.\n\nRepo: [github.com/social-media-skills/youtube-skill](https://github.com/social-media-skills/youtube-skill)\n\n*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.*\n\nThe 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.\n\n``` php\nfinal.mp4\n  -> check_setup.py           Python, ffmpeg, Pillow, transcription routes, OpenCV (optional)\n  -> transcribe.py            work/transcript.json + transcript.txt\n       --from captions.srt | GROQ_API_KEY | OPENAI_API_KEY | local faster-whisper\n  -> suggest.py               pack/suggestions.md: what people type into YouTube search\n  -> agent reads the whole transcript, picks one phrase, writes upload.json\n  -> youtube_pack.py build    checks every field, snaps chapters, writes description.txt,\n                              captions.srt, post.json, manifest.json, upload-pack.md\n  -> youtube_pack.py frames   frames.jpg: 12 scored candidate frames\n  -> agent writes thumbs.json (2 or 3 different ideas)\n  -> thumbnail.py             thumb-a.jpg, thumb-b.jpg ... and preview.png at feed size\n  -> you approve\n  -> WoopSocial               upload session, posts_validate, posts_create\n```\n\nApart from Pillow, the scripts use only the standard library and ffmpeg. No paid API is required anywhere in the chain.\n\n`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.\n\nThat 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.\n\nThe 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.\n\nWith 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.\n\nThe 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.\n\n| Field | What the script measures | Warns when | \n|---|---|---|\n| Title | characters | over 100 (hard), over 70 (usually cut), phrase missing, mostly capitals, `<` or`>` | \n| Description | UTF-8 bytes | over 5,000 bytes, phrase not in the first 150 characters, `<` or`>` | \n| Tags | characters as the API counts them | over 500, or a tag contains `<` ,`>` or a comma | \n| Hashtags | count and characters | a space or symbol breaks one, or more than 60 (all are then ignored) | \n| Chapters | seconds | not at 0:00, fewer than 3, any under 10 s, any after the end | \n| Made for kids | presence | missing; it's a legal setting the user must declare | \n| Schedule | ISO time | no timezone, or in the past | \n\nThe 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:\n\n``` python\ndef tags_length(tags):\n    return sum(len(t) + (2 if \" \" in t else 0) for t in tags) + max(0, len(tags) - 1)\n```\n\nThe 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.\n\nThe 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.\n\nA 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:\n\n```\nfor t, name in chapters:\n    s = secs(t)\n    if sentences and s > 0:\n        near = min(sentences, key=lambda x: abs(x[\"start\"] - s))\n        if abs(near[\"start\"] - s) <= 3.0:\n            s = max(0.0, near[\"start\"] - 0.3)\n```\n\nIn 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.\n\n`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:\n\n```\nscore = (min(sharp, 2500) / 2500) * 0.5 \\\n      + (min(contrast, 70) / 70) * 0.3 \\\n      + (min(hsv, 120) / 120) * 0.2\nif mean < 25 or mean > 235:\n    score *= 0.2          # black or blown-out frames\nif faces:\n    score += 0.15 + min(face_share * 2, 0.2)\n```\n\nSharpness 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.\n\nA 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.\n\nThe 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.\n\nThe 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.\n\nYouTube 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:\n\n``` python\ndef save_jpeg(im, path):\n    while True:\n        for q in (92, 88, 84, 80, 76, 72):\n            im.save(path, \"JPEG\", quality=q, optimize=True, progressive=True)\n            if os.path.getsize(path) <= MAX_BYTES:\n                return q, im.size, True\n        # still too big: step down 2160 -> 1440 -> 1080 -> 720, then lower quality at 720\n```\n\nThe example's source is 1080p, so its three thumbnails came out at 1920x1080 and quality 92, between about 150 and 190 KB each.\n\nLayout 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.\n\nA 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.\n\nA 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.\n\nThumbnails 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.\n\n`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.\n\nThe 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.\n\n`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.\n\nThe 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.\n\nThe free plan covers 2 social accounts, with everything from posting to analytics included, and setup for each agent is at [woopsocial.com/agents](https://woopsocial.com/agents?utm_source=youtube-skill&utm_medium=devto). Disclosure: the skill is maintained by Frank Heijdenrijk and the team behind [WoopSocial](https://woopsocial.com/?utm_source=youtube-skill&utm_medium=devto). Uploading is optional; without it the skill hands you the pack to upload yourself.\n\n```\nnpx skills add social-media-skills/youtube-skill -g -a claude-code -y\n```\n\nOpenClaw: `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.\n\nThen hand it a finished video: \"put final.mp4 on my channel and make it findable.\" Issues and pull requests are welcome on [GitHub](https://github.com/social-media-skills/youtube-skill), and more free skills for social media work are at [social-media-skills.com](https://social-media-skills.com).", "url": "https://wpnews.pro/news/youtube-skill-for-claude-code-how-it-picks-the-search-phrase-counts-youtube-s", "canonical_source": "https://dev.to/fheijdenrijk/youtube-skill-for-claude-code-how-it-picks-the-search-phrase-counts-youtubes-limits-and-scores-ac0", "published_at": "2026-11-02 09:38:04+00:00", "updated_at": "2026-11-02 09:52:43.229715+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "generative-ai"], "entities": ["Claude Code", "Codex", "Cursor", "OpenClaw", "YouTube", "WoopSocial", "NASA", "JPL"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/youtube-skill-for-claude-code-how-it-picks-the-search-phrase-counts-youtube-s", "markdown": "https://wpnews.pro/news/youtube-skill-for-claude-code-how-it-picks-the-search-phrase-counts-youtube-s.md", "text": "https://wpnews.pro/news/youtube-skill-for-claude-code-how-it-picks-the-search-phrase-counts-youtube-s.txt", "jsonld": "https://wpnews.pro/news/youtube-skill-for-claude-code-how-it-picks-the-search-phrase-counts-youtube-s.jsonld"}}