
Content Strategist
- 49 installs
- 451 repo stars
- Updated July 21, 2026
- borghei/claude-skills
Video Content Strategist is a Claude skill that plans video content calendars, analyzes thumbnails, and optimizes video titles, descriptions, and tags for platform SEO.
About
Video Content Strategist is a video-content planning toolkit for content calendars, thumbnail analysis, and video metadata SEO. A creator or marketing team uses it to plan a multi-week content calendar from audience and pillar data, analyze thumbnail patterns, and optimize titles, descriptions, and tags across YouTube, TikTok, LinkedIn, and Instagram. It ships Python scripts for planning, thumbnail analysis, and SEO optimization.
- Plans video content calendars from topics, audience, and pillar ratios
- Analyzes thumbnail composition and optimizes video titles, descriptions, and tags for SEO
- Supports YouTube, TikTok, LinkedIn, and Instagram with platform-specific length guidance
Content Strategist by the numbers
- 49 all-time installs (skills.sh)
- Ranked #1,328 of 1,879 Marketing & SEO skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
content-strategist capabilities & compatibility
- Capabilities
- content calendar · video seo · thumbnail analysis
- Works with
- Use cases
- seo · marketing · copywriting
- Pricing
- Free
What content-strategist says it does
Production-ready video content strategy toolkit for planning content calendars, analyzing thumbnail effectiveness, and optimizing video metadata for platform SEO.
Optimize video titles, descriptions, and tags for SEO
npx skills add https://github.com/borghei/claude-skills --skill content-strategistAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 49 |
|---|---|
| repo stars | ★ 451 |
| Last updated | July 21, 2026 |
| Repository | borghei/claude-skills ↗ |
What it does
Plan a video content calendar, analyze thumbnails, and optimize video metadata for platform SEO.
Who is it for?
Creators and marketing teams managing consistent video output across platforms
Skip if: Written blog content or long-form article production
When should I use this skill?
Planning video content, building a video calendar, or optimizing video SEO and thumbnails
What you get
Produces a platform-adapted content calendar plus optimized titles, descriptions, tags, and thumbnail guidance.
- video content calendar
- thumbnail optimization recommendations
- optimized video titles, descriptions, and tags
By the numbers
- Ships 3 tools: content planner, thumbnail analyzer, SEO optimizer
- Provides length guidelines for 5 platforms
Files
Video Content Strategist Skill
Overview
Production-ready video content strategy toolkit for planning content calendars, analyzing thumbnail effectiveness, and optimizing video metadata for platform SEO. Designed for content creators, marketing teams, and video producers managing consistent video output across YouTube, TikTok, LinkedIn, and other platforms.
Clarify First
Before planning, confirm these inputs. If any is unknown or vague, ASK — do not assume:
- [ ] Platform(s) — YouTube, TikTok, LinkedIn, or Instagram — sets optimal video length and SEO approach
- [ ] Deliverable — content calendar, thumbnail analysis, or SEO metadata optimization — selects the tool and workflow
- [ ] Audience & content pillars — who it's for and the pillar mix/ratios — drives calendar topics and formats
- [ ] Posting frequency — videos per week and planning horizon — sets the production schedule
Stop rule: ask only the 2-3 that most change the output. If the user says "just draft it," proceed and list your assumptions at the top of the artifact.
Quick Start
# Plan a video content calendar from topics and audience data
python scripts/video_content_planner.py topics.json --weeks 8 --frequency 3
# Analyze thumbnail text and composition patterns
python scripts/thumbnail_analyzer.py thumbnails.csv
# Optimize video titles, descriptions, and tags for SEO
python scripts/video_seo_optimizer.py video_data.json --platform youtubeTools Overview
| Tool | Purpose | Input | Output |
|---|---|---|---|
video_content_planner.py | Content calendar generation | JSON with topics/audience | Weekly calendar + production schedule |
thumbnail_analyzer.py | Thumbnail pattern analysis | CSV with thumbnail data | Optimization recommendations |
video_seo_optimizer.py | Video metadata SEO | JSON with video details | Optimized titles, descriptions, tags |
Workflows
Workflow 1: Monthly Video Strategy
1. Define audience personas and content pillars in topics JSON 2. Run video_content_planner.py to generate 4-week calendar 3. For each planned video, run video_seo_optimizer.py for metadata 4. After publishing, collect thumbnail data and run thumbnail_analyzer.py 5. Feed learnings back into next month's planning cycle
Workflow 2: YouTube Channel Optimization
1. Export existing video data (titles, descriptions, tags, performance) 2. Run video_seo_optimizer.py on underperforming videos to identify metadata gaps 3. Run thumbnail_analyzer.py on top vs bottom performers 4. Apply optimizations to existing videos and use patterns for new content
Workflow 3: Multi-Platform Video Strategy
1. Create topics JSON with platform-specific audience data 2. Run video_content_planner.py with --platforms youtube,tiktok,linkedin 3. Get platform-adapted content recommendations 4. Optimize each platform's metadata with video_seo_optimizer.py
Reference Documentation
See references/video-strategy-guide.md for comprehensive frameworks covering:
- Content pillar strategy
- Platform-specific best practices
- Thumbnail design principles
- Video SEO fundamentals
- Production workflow optimization
Common Patterns
Pattern: Topics JSON Format
{
"channel": "TechStartupTV",
"audience": {
"primary": "SaaS founders, 25-45",
"interests": ["startup growth", "fundraising", "product development"],
"pain_points": ["scaling teams", "finding product-market fit", "managing burn rate"]
},
"content_pillars": [
{"name": "Founder Stories", "ratio": 0.3, "format": "interview", "avg_length_min": 25},
{"name": "Tactical Guides", "ratio": 0.4, "format": "tutorial", "avg_length_min": 12},
{"name": "Industry Analysis", "ratio": 0.2, "format": "commentary", "avg_length_min": 8},
{"name": "Behind the Scenes", "ratio": 0.1, "format": "vlog", "avg_length_min": 5}
],
"topics": [
{"title": "How We Hit $1M ARR", "pillar": "Founder Stories", "priority": "high"},
{"title": "5 Pricing Strategies That Work", "pillar": "Tactical Guides", "priority": "high"},
{"title": "AI in SaaS: 2026 Trends", "pillar": "Industry Analysis", "priority": "medium"}
]
}Pattern: Thumbnail CSV Format
video_id,title,views,ctr_pct,has_face,has_text,text_words,colors_dominant,emotion
V001,How to Scale,15000,8.2,yes,yes,3,red-yellow,surprise
V002,Tech Review,8500,4.1,no,yes,5,blue-white,neutralPlatform Video Length Guidelines
| Platform | Optimal Length | Max Recommended |
|---|---|---|
| YouTube (standard) | 8-15 min | 25 min |
| YouTube Shorts | 30-60 sec | 60 sec |
| TikTok | 30-90 sec | 3 min |
| 1-3 min | 10 min | |
| Instagram Reels | 15-60 sec | 90 sec |
Video Content Strategy Guide
Content Pillar Framework
Defining Content Pillars
Content pillars are 3-5 recurring themes that form the backbone of your video strategy. Each pillar should:
1. Align with audience needs - Address specific pain points or aspirations 2. Support business goals - Drive awareness, consideration, or conversion 3. Be sustainable - Generate enough topic ideas for 6+ months 4. Differentiate - Offer unique perspective vs competitors
Recommended Pillar Distribution
- Educational/How-To (30-40%) - Highest search volume, evergreen value
- Story/Interview (20-30%) - Builds trust, drives engagement
- Commentary/Analysis (15-20%) - Establishes authority, timely relevance
- Behind-the-Scenes/Culture (10-15%) - Humanizes brand, builds community
Content Mix by Funnel Stage
- Top of Funnel (50%): Broad topics, search-optimized, awareness
- Middle of Funnel (30%): Specific solutions, comparison content
- Bottom of Funnel (20%): Case studies, demos, conversion content
Platform-Specific Best Practices
YouTube
Algorithm factors:
- Click-through rate (CTR) from impressions
- Average view duration and retention curve
- Session time (do viewers watch more after your video?)
- Engagement (likes, comments, shares, subscribes)
Optimization priorities: 1. Thumbnail (drives CTR, #1 lever) 2. Title (search + browse discovery) 3. First 30 seconds (determines retention) 4. Description and tags (search ranking) 5. End screens and cards (session continuity)
Posting cadence: 1-3 videos per week, consistent schedule
TikTok
Algorithm factors:
- Watch completion rate (most important)
- Replay rate
- Shares (weighted heavily)
- Comments and engagement
- Profile visits after watching
Optimization priorities: 1. Hook in first 1-3 seconds 2. Trending sounds and effects 3. Hashtag strategy (mix niche + broad) 4. Post frequency (daily if possible)
LinkedIn Video
Algorithm factors:
- Dwell time on post
- Comments (especially long-form)
- Shares to external networks
- Connection engagement
Optimization priorities: 1. Native upload (not YouTube links) 2. Professional but authentic tone 3. Captions/subtitles mandatory (80% watch muted) 4. Text post paired with video
Thumbnail Design Principles
Elements That Drive CTR
1. Faces with emotion - Thumbnails with faces get 38% higher CTR
- Surprise and curiosity expressions perform best
- Eye contact with camera creates connection
- Avoid neutral expressions
2. Bold, readable text - 3-5 words maximum
- High contrast against background
- Font size readable at mobile thumbnail size
- Complement (not duplicate) the title
3. Color contrast - Stand out in the feed
- Bright colors (red, yellow, orange) attract attention
- Complementary color schemes
- Avoid colors that blend with platform UI
4. Visual clarity - Simple beats complex
- One clear focal point
- Minimal clutter
- Before/after or transformation visuals work well
5. Brand consistency - Recognizable patterns
- Consistent style template
- Brand colors and fonts
- Series-specific design elements
Thumbnail Anti-Patterns
- Too many text words (>5)
- Small text unreadable on mobile
- Clickbait that doesn't match content
- Generic stock imagery
- No contrast with background
- Cluttered composition with multiple focal points
Video SEO Fundamentals
Title Optimization
- Front-load keywords - Put primary keyword in first 5 words
- Include numbers - "5 Ways to..." or "in 10 Minutes" perform well
- Create curiosity gap - Hint at value without revealing everything
- 60 characters max - Truncation happens on mobile around 60-65 characters
- Avoid ALL CAPS for entire title (one word emphasis is okay)
Description Optimization
- First 2-3 lines visible - Put hook and primary keywords here
- Include target keywords naturally - 2-3 times in 200-300 word description
- Timestamps - Add chapter markers for longer videos (improves search appearance)
- Links and CTAs - Place after first paragraph
- Related content links - Drive session time
Tag Strategy
- Primary keyword as first tag
- Long-tail variations - 5-10 specific phrases
- Broad category tags - 2-3 for topical relevance
- Brand tags - Channel name, series name
- 15-20 tags total - YouTube has 500 character limit
Search Intent Matching
- How-to intent - Tutorial titles, step-by-step structure
- Review intent - Comparison titles, honest assessment framing
- Entertainment intent - Story-driven titles, curiosity hooks
- News intent - Timely titles, date references
Production Workflow Optimization
Pre-Production Checklist
1. Topic research and keyword validation 2. Script or outline creation 3. Thumbnail concept design 4. Equipment and location prep 5. Guest coordination (if applicable)
Production Efficiency Tips
- Batch filming - Record 3-4 videos per session
- Template scripts - Reuse intro/outro structures
- Lighting setup persistence - Keep studio ready
- B-roll library - Build reusable footage collection
Post-Production Checklist
1. Rough cut and structure review 2. Graphics, lower thirds, and callouts 3. Audio cleanup and music 4. Thumbnail finalization 5. Metadata preparation (title, description, tags) 6. Scheduling and cross-promotion
#!/usr/bin/env python3
"""
Thumbnail Analyzer
Analyzes thumbnail text, composition patterns, and their correlation with
video performance (CTR, views). Identifies which thumbnail characteristics
drive higher click-through rates.
Expected CSV columns: video_id, title, views, ctr_pct, has_face, has_text,
text_words, colors_dominant, emotion
Usage:
python thumbnail_analyzer.py thumbnails.csv
python thumbnail_analyzer.py thumbnails.csv --format json
python thumbnail_analyzer.py thumbnails.csv --min-views 1000
python thumbnail_analyzer.py thumbnails.csv --top 10
"""
import argparse
import csv
import json
import statistics
import sys
from collections import defaultdict
from typing import Any, Dict, List, Optional, Tuple
def load_thumbnail_data(filepath: str) -> List[Dict[str, Any]]:
"""Load thumbnail data from CSV."""
data = []
with open(filepath, "r", newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
try:
entry = {
"video_id": row.get("video_id", "").strip(),
"title": row.get("title", "").strip(),
"views": int(row.get("views", 0)),
"ctr_pct": float(row.get("ctr_pct", 0)),
"has_face": row.get("has_face", "").strip().lower() in ("yes", "true", "1"),
"has_text": row.get("has_text", "").strip().lower() in ("yes", "true", "1"),
"text_words": int(row.get("text_words", 0)),
"colors_dominant": row.get("colors_dominant", "").strip().lower(),
"emotion": row.get("emotion", "neutral").strip().lower(),
}
data.append(entry)
except (ValueError, KeyError):
continue
return data
def calculate_stats(values: List[float]) -> Dict[str, float]:
"""Calculate basic statistics for a list of values."""
if not values:
return {"count": 0, "mean": 0, "median": 0, "stdev": 0, "min": 0, "max": 0}
return {
"count": len(values),
"mean": round(statistics.mean(values), 2),
"median": round(statistics.median(values), 2),
"stdev": round(statistics.stdev(values), 2) if len(values) > 1 else 0,
"min": round(min(values), 2),
"max": round(max(values), 2),
}
def analyze_by_attribute(data: List[Dict], attribute: str, value_key: str = "ctr_pct") -> Dict[str, Any]:
"""Analyze performance grouped by a boolean or categorical attribute."""
groups = defaultdict(list)
for entry in data:
val = entry.get(attribute)
if isinstance(val, bool):
key = "yes" if val else "no"
else:
key = str(val) if val else "unknown"
groups[key].append(entry[value_key])
result = {}
for key, values in sorted(groups.items()):
result[key] = calculate_stats(values)
return result
def analyze_text_word_count(data: List[Dict]) -> Dict[str, Any]:
"""Analyze CTR by number of text words on thumbnail."""
buckets = {"0 words": [], "1-3 words": [], "4-5 words": [], "6+ words": []}
for entry in data:
wc = entry["text_words"]
ctr = entry["ctr_pct"]
if wc == 0:
buckets["0 words"].append(ctr)
elif wc <= 3:
buckets["1-3 words"].append(ctr)
elif wc <= 5:
buckets["4-5 words"].append(ctr)
else:
buckets["6+ words"].append(ctr)
return {k: calculate_stats(v) for k, v in buckets.items()}
def analyze_color_performance(data: List[Dict]) -> Dict[str, Any]:
"""Analyze CTR by dominant thumbnail colors."""
color_ctrs = defaultdict(list)
for entry in data:
colors = entry["colors_dominant"]
if not colors:
continue
# Handle multi-color entries like "red-yellow"
for color in colors.replace(",", "-").split("-"):
color = color.strip()
if color:
color_ctrs[color].append(entry["ctr_pct"])
return {color: calculate_stats(ctrs) for color, ctrs in sorted(color_ctrs.items())}
def find_top_performers(data: List[Dict], n: int = 10) -> List[Dict]:
"""Find top N performing thumbnails by CTR."""
sorted_data = sorted(data, key=lambda x: x["ctr_pct"], reverse=True)
return sorted_data[:n]
def find_bottom_performers(data: List[Dict], n: int = 10) -> List[Dict]:
"""Find bottom N performing thumbnails by CTR."""
sorted_data = sorted(data, key=lambda x: x["ctr_pct"])
return sorted_data[:n]
def generate_recommendations(analysis: Dict[str, Any]) -> List[Dict[str, str]]:
"""Generate actionable recommendations from analysis."""
recs = []
# Face analysis
face_data = analysis.get("face_analysis", {})
if "yes" in face_data and "no" in face_data:
face_ctr = face_data["yes"]["mean"]
no_face_ctr = face_data["no"]["mean"]
if face_ctr > no_face_ctr * 1.1:
lift = ((face_ctr - no_face_ctr) / no_face_ctr * 100)
recs.append({
"category": "Faces",
"recommendation": f"Include faces in thumbnails - {lift:.0f}% higher CTR with faces "
f"({face_ctr:.1f}% vs {no_face_ctr:.1f}%)",
"priority": "high",
})
elif no_face_ctr > face_ctr * 1.1:
recs.append({
"category": "Faces",
"recommendation": "Thumbnails without faces perform better for your content - "
"consider graphic/text-focused designs",
"priority": "medium",
})
# Text analysis
text_data = analysis.get("text_analysis", {})
if "yes" in text_data and "no" in text_data:
text_ctr = text_data["yes"]["mean"]
no_text_ctr = text_data["no"]["mean"]
if text_ctr > no_text_ctr * 1.05:
recs.append({
"category": "Text",
"recommendation": f"Text overlays improve CTR ({text_ctr:.1f}% vs {no_text_ctr:.1f}%)",
"priority": "high",
})
# Word count analysis
wc_data = analysis.get("word_count_analysis", {})
if wc_data:
best_bucket = max(
[(k, v["mean"]) for k, v in wc_data.items() if v["count"] >= 3],
key=lambda x: x[1],
default=(None, 0),
)
if best_bucket[0]:
recs.append({
"category": "Text Length",
"recommendation": f"Optimal text length: {best_bucket[0]} (avg CTR: {best_bucket[1]:.1f}%)",
"priority": "medium",
})
# Emotion analysis
emotion_data = analysis.get("emotion_analysis", {})
if emotion_data:
valid_emotions = [(k, v["mean"]) for k, v in emotion_data.items() if v["count"] >= 3]
if valid_emotions:
best_emotion = max(valid_emotions, key=lambda x: x[1])
if best_emotion[1] > 0:
recs.append({
"category": "Emotion",
"recommendation": f"Best performing emotion: '{best_emotion[0]}' "
f"(avg CTR: {best_emotion[1]:.1f}%)",
"priority": "medium",
})
# Color analysis
color_data = analysis.get("color_analysis", {})
if color_data:
valid_colors = [(k, v["mean"]) for k, v in color_data.items() if v["count"] >= 3]
if valid_colors:
top_colors = sorted(valid_colors, key=lambda x: x[1], reverse=True)[:3]
color_list = ", ".join(f"{c[0]} ({c[1]:.1f}%)" for c in top_colors)
recs.append({
"category": "Colors",
"recommendation": f"Top performing colors: {color_list}",
"priority": "low",
})
return recs
def print_human(analysis: Dict[str, Any], top: List[Dict], bottom: List[Dict],
recs: List[Dict]) -> None:
"""Print analysis in human-readable format."""
print("=" * 70)
print(" Thumbnail Performance Analysis")
print("=" * 70)
overall = analysis["overall_stats"]
print(f"\n Total Thumbnails Analyzed: {overall['count']}")
print(f" Average CTR: {overall['mean']:.2f}%")
print(f" Median CTR: {overall['median']:.2f}%")
print(f" CTR Range: {overall['min']:.2f}% - {overall['max']:.2f}%")
# Face analysis
print(f"\n --- Face Presence ---")
for key, stats in analysis.get("face_analysis", {}).items():
print(f" {key:<6}: avg CTR {stats['mean']:.2f}% (n={stats['count']})")
# Text analysis
print(f"\n --- Text Overlay ---")
for key, stats in analysis.get("text_analysis", {}).items():
print(f" {key:<6}: avg CTR {stats['mean']:.2f}% (n={stats['count']})")
# Word count
print(f"\n --- Text Word Count ---")
for bucket, stats in analysis.get("word_count_analysis", {}).items():
if stats["count"] > 0:
bar = "#" * max(1, int(stats["mean"] * 2))
print(f" {bucket:<12}: avg CTR {stats['mean']:.2f}% (n={stats['count']}) {bar}")
# Emotion
print(f"\n --- Emotion ---")
for emotion, stats in sorted(analysis.get("emotion_analysis", {}).items(),
key=lambda x: x[1]["mean"], reverse=True):
if stats["count"] > 0:
bar = "#" * max(1, int(stats["mean"] * 2))
print(f" {emotion:<12}: avg CTR {stats['mean']:.2f}% (n={stats['count']}) {bar}")
# Colors
print(f"\n --- Dominant Colors ---")
for color, stats in sorted(analysis.get("color_analysis", {}).items(),
key=lambda x: x[1]["mean"], reverse=True):
if stats["count"] > 0:
bar = "#" * max(1, int(stats["mean"] * 2))
print(f" {color:<12}: avg CTR {stats['mean']:.2f}% (n={stats['count']}) {bar}")
# Top performers
print(f"\n --- Top Performers ---")
for i, t in enumerate(top[:5], 1):
face = "Face" if t["has_face"] else "No face"
text_info = f"{t['text_words']} words" if t["has_text"] else "No text"
print(f" {i}. {t['title'][:35]:<35} CTR: {t['ctr_pct']:.1f}% ({face}, {text_info})")
# Bottom performers
print(f"\n --- Bottom Performers ---")
for i, t in enumerate(bottom[:5], 1):
face = "Face" if t["has_face"] else "No face"
text_info = f"{t['text_words']} words" if t["has_text"] else "No text"
print(f" {i}. {t['title'][:35]:<35} CTR: {t['ctr_pct']:.1f}% ({face}, {text_info})")
# Recommendations
if recs:
print(f"\n --- Recommendations ---")
priority_order = {"high": 0, "medium": 1, "low": 2}
for rec in sorted(recs, key=lambda r: priority_order.get(r["priority"], 9)):
marker = {"high": "[!!!]", "medium": "[ ! ]", "low": "[ ]"}.get(rec["priority"], "[ ]")
print(f" {marker} {rec['category']}: {rec['recommendation']}")
print()
def main():
parser = argparse.ArgumentParser(
description="Analyze thumbnail text and composition patterns for optimization"
)
parser.add_argument("file", help="CSV file with thumbnail data")
parser.add_argument("--format", choices=["human", "json"], default="human", help="Output format")
parser.add_argument("--min-views", type=int, default=0, help="Minimum views filter")
parser.add_argument("--top", type=int, default=10, help="Number of top/bottom performers to show")
args = parser.parse_args()
data = load_thumbnail_data(args.file)
if not data:
print("Error: No valid thumbnail data found", file=sys.stderr)
sys.exit(1)
if args.min_views > 0:
data = [d for d in data if d["views"] >= args.min_views]
if not data:
print(f"Error: No thumbnails with >= {args.min_views} views", file=sys.stderr)
sys.exit(1)
ctrs = [d["ctr_pct"] for d in data]
analysis = {
"overall_stats": calculate_stats(ctrs),
"face_analysis": analyze_by_attribute(data, "has_face"),
"text_analysis": analyze_by_attribute(data, "has_text"),
"word_count_analysis": analyze_text_word_count(data),
"emotion_analysis": analyze_by_attribute(data, "emotion"),
"color_analysis": analyze_color_performance(data),
}
top = find_top_performers(data, args.top)
bottom = find_bottom_performers(data, args.top)
recs = generate_recommendations(analysis)
if args.format == "json":
output = {
"analysis": analysis,
"top_performers": top,
"bottom_performers": bottom,
"recommendations": recs,
}
print(json.dumps(output, indent=2, default=str))
else:
print_human(analysis, top, bottom, recs)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Video Content Planner
Plans video content calendar from topics, audience data, and content pillars.
Distributes topics across weeks, assigns production slots, and balances
content mix by pillar and funnel stage.
Expected JSON input with: channel, audience, content_pillars, topics
Usage:
python video_content_planner.py topics.json
python video_content_planner.py topics.json --weeks 8 --frequency 3
python video_content_planner.py topics.json --platforms youtube,tiktok
python video_content_planner.py topics.json --format json
"""
import argparse
import json
import sys
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple
PLATFORM_DEFAULTS = {
"youtube": {"optimal_length_min": 12, "max_length_min": 25, "best_days": ["Tuesday", "Thursday", "Saturday"]},
"tiktok": {"optimal_length_min": 1, "max_length_min": 3, "best_days": ["Tuesday", "Wednesday", "Thursday", "Friday"]},
"linkedin": {"optimal_length_min": 2, "max_length_min": 10, "best_days": ["Tuesday", "Wednesday", "Thursday"]},
"instagram": {"optimal_length_min": 1, "max_length_min": 1.5, "best_days": ["Monday", "Wednesday", "Friday"]},
"shorts": {"optimal_length_min": 0.75, "max_length_min": 1, "best_days": ["Monday", "Wednesday", "Friday", "Saturday"]},
}
FUNNEL_STAGES = {
"tofu": {"label": "Top of Funnel", "target_ratio": 0.50},
"mofu": {"label": "Middle of Funnel", "target_ratio": 0.30},
"bofu": {"label": "Bottom of Funnel", "target_ratio": 0.20},
}
DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
def load_topics(filepath: str) -> Dict[str, Any]:
"""Load topics and channel data from JSON."""
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
def assign_funnel_stage(topic: Dict) -> str:
"""Assign funnel stage based on topic properties."""
stage = topic.get("funnel_stage", "").lower()
if stage in FUNNEL_STAGES:
return stage
# Infer from format/pillar
fmt = topic.get("format", "").lower()
pillar = topic.get("pillar", "").lower()
if any(kw in pillar for kw in ["how", "tutorial", "guide", "education"]):
return "tofu"
if any(kw in pillar for kw in ["case study", "demo", "review", "comparison"]):
return "mofu"
if any(kw in pillar for kw in ["testimonial", "conversion", "pricing"]):
return "bofu"
if fmt in ("tutorial", "commentary"):
return "tofu"
if fmt in ("interview", "vlog"):
return "mofu"
return "tofu"
def prioritize_topics(topics: List[Dict]) -> List[Dict]:
"""Sort topics by priority score."""
priority_values = {"high": 3, "medium": 2, "low": 1}
return sorted(topics, key=lambda t: priority_values.get(t.get("priority", "medium"), 2), reverse=True)
def distribute_to_calendar(
topics: List[Dict],
pillars: List[Dict],
weeks: int,
frequency: int,
platforms: List[str],
start_date: datetime,
) -> List[Dict]:
"""Distribute topics across weekly calendar slots."""
sorted_topics = prioritize_topics(list(topics))
# Build pillar lookup
pillar_map = {}
for p in pillars:
pillar_map[p["name"].lower()] = p
# Calculate slots
total_slots = weeks * frequency
calendar = []
topic_idx = 0
used_pillars = defaultdict(int)
for week in range(weeks):
week_start = start_date + timedelta(weeks=week)
week_entries = []
# Determine which days to publish this week
primary_platform = platforms[0] if platforms else "youtube"
best_days = PLATFORM_DEFAULTS.get(primary_platform, {}).get("best_days", ["Tuesday", "Thursday"])
publish_days = []
for day_name in best_days[:frequency]:
day_idx = DAY_NAMES.index(day_name)
publish_date = week_start + timedelta(days=(day_idx - week_start.weekday()) % 7)
if publish_date < week_start:
publish_date += timedelta(weeks=1)
publish_days.append(publish_date)
# Fill remaining slots if frequency > best_days
while len(publish_days) < frequency:
for day_idx in range(7):
candidate = week_start + timedelta(days=day_idx)
if candidate not in publish_days:
publish_days.append(candidate)
if len(publish_days) >= frequency:
break
publish_days.sort()
for slot_idx, pub_date in enumerate(publish_days):
if topic_idx < len(sorted_topics):
topic = sorted_topics[topic_idx]
topic_idx += 1
else:
# Generate placeholder for remaining slots
topic = {
"title": f"[TBD] Week {week + 1} Slot {slot_idx + 1}",
"pillar": pillars[slot_idx % len(pillars)]["name"] if pillars else "General",
"priority": "low",
"placeholder": True,
}
pillar_name = topic.get("pillar", "General")
pillar_info = pillar_map.get(pillar_name.lower(), {})
funnel = assign_funnel_stage(topic)
used_pillars[pillar_name] += 1
# Calculate production dates
script_due = pub_date - timedelta(days=7)
film_date = pub_date - timedelta(days=4)
edit_due = pub_date - timedelta(days=2)
entry = {
"week": week + 1,
"publish_date": pub_date.strftime("%Y-%m-%d"),
"publish_day": DAY_NAMES[pub_date.weekday()],
"title": topic.get("title", "Untitled"),
"pillar": pillar_name,
"format": topic.get("format", pillar_info.get("format", "standard")),
"funnel_stage": funnel,
"priority": topic.get("priority", "medium"),
"est_length_min": pillar_info.get("avg_length_min", 10),
"platforms": platforms,
"production_schedule": {
"script_due": script_due.strftime("%Y-%m-%d"),
"film_date": film_date.strftime("%Y-%m-%d"),
"edit_due": edit_due.strftime("%Y-%m-%d"),
},
"placeholder": topic.get("placeholder", False),
}
calendar.append(entry)
return calendar
def analyze_calendar_balance(calendar: List[Dict], pillars: List[Dict]) -> Dict[str, Any]:
"""Analyze content mix balance in the calendar."""
pillar_counts = defaultdict(int)
funnel_counts = defaultdict(int)
format_counts = defaultdict(int)
total = len(calendar)
for entry in calendar:
if not entry.get("placeholder"):
pillar_counts[entry["pillar"]] += 1
funnel_counts[entry["funnel_stage"]] += 1
format_counts[entry["format"]] += 1
actual_total = sum(pillar_counts.values())
# Compare pillar distribution to targets
pillar_analysis = {}
for p in pillars:
name = p["name"]
target = p.get("ratio", 1 / len(pillars))
actual = pillar_counts.get(name, 0) / actual_total if actual_total > 0 else 0
deviation = abs(actual - target)
pillar_analysis[name] = {
"target_pct": round(target * 100, 1),
"actual_pct": round(actual * 100, 1),
"count": pillar_counts.get(name, 0),
"balanced": deviation < 0.10,
}
# Funnel analysis
funnel_analysis = {}
for stage, info in FUNNEL_STAGES.items():
actual = funnel_counts.get(stage, 0) / actual_total if actual_total > 0 else 0
funnel_analysis[stage] = {
"label": info["label"],
"target_pct": round(info["target_ratio"] * 100, 1),
"actual_pct": round(actual * 100, 1),
"count": funnel_counts.get(stage, 0),
}
issues = []
for name, data in pillar_analysis.items():
if not data["balanced"]:
if data["actual_pct"] < data["target_pct"] - 10:
issues.append(f"Under-represented pillar: {name} ({data['actual_pct']}% vs {data['target_pct']}% target)")
elif data["actual_pct"] > data["target_pct"] + 10:
issues.append(f"Over-represented pillar: {name} ({data['actual_pct']}% vs {data['target_pct']}% target)")
return {
"total_videos": total,
"planned_videos": actual_total,
"placeholder_videos": total - actual_total,
"pillar_balance": pillar_analysis,
"funnel_balance": funnel_analysis,
"format_mix": dict(format_counts),
"issues": issues,
}
def print_human(calendar: List[Dict], analysis: Dict, channel: str) -> None:
"""Print calendar in human-readable format."""
print("=" * 76)
print(f" Video Content Calendar - {channel}")
print("=" * 76)
print(f"\n Total Videos: {analysis['total_videos']} ({analysis['planned_videos']} planned, "
f"{analysis['placeholder_videos']} TBD)")
current_week = 0
for entry in calendar:
if entry["week"] != current_week:
current_week = entry["week"]
print(f"\n --- Week {current_week} ---")
placeholder = " [TBD]" if entry.get("placeholder") else ""
print(f" {entry['publish_date']} ({entry['publish_day']:<9}): {entry['title']}{placeholder}")
print(f" Pillar: {entry['pillar']} | Format: {entry['format']} | "
f"Funnel: {entry['funnel_stage']} | ~{entry['est_length_min']} min")
sched = entry["production_schedule"]
print(f" Script: {sched['script_due']} | Film: {sched['film_date']} | Edit: {sched['edit_due']}")
# Balance analysis
print(f"\n {'=' * 72}")
print(f" CONTENT MIX ANALYSIS")
print(f" {'=' * 72}")
print(f"\n Pillar Distribution:")
for name, data in analysis["pillar_balance"].items():
status = "OK" if data["balanced"] else "!!"
bar = "#" * max(1, int(data["actual_pct"] / 3))
print(f" [{status}] {name:<25} {data['actual_pct']:>5.1f}% (target: {data['target_pct']:.1f}%) {bar}")
print(f"\n Funnel Distribution:")
for stage, data in analysis["funnel_balance"].items():
bar = "#" * max(1, int(data["actual_pct"] / 3))
print(f" {data['label']:<20} {data['actual_pct']:>5.1f}% (target: {data['target_pct']:.1f}%) {bar}")
if analysis["issues"]:
print(f"\n Issues:")
for issue in analysis["issues"]:
print(f" [!] {issue}")
print()
def main():
parser = argparse.ArgumentParser(
description="Plan video content calendar from topics and audience data"
)
parser.add_argument("file", help="JSON file with topics and channel data")
parser.add_argument("--format", choices=["human", "json"], default="human", help="Output format")
parser.add_argument("--weeks", type=int, default=4, help="Number of weeks to plan (default: 4)")
parser.add_argument("--frequency", type=int, default=2,
help="Videos per week (default: 2)")
parser.add_argument("--platforms", default="youtube",
help="Comma-separated target platforms (default: youtube)")
parser.add_argument("--start-date", help="Start date YYYY-MM-DD (default: next Monday)")
args = parser.parse_args()
data = load_topics(args.file)
channel = data.get("channel", "My Channel")
pillars = data.get("content_pillars", [])
topics = data.get("topics", [])
platforms = [p.strip() for p in args.platforms.split(",")]
if args.start_date:
start = datetime.strptime(args.start_date, "%Y-%m-%d")
else:
today = datetime.now()
days_until_monday = (7 - today.weekday()) % 7
if days_until_monday == 0:
days_until_monday = 7
start = today + timedelta(days=days_until_monday)
calendar = distribute_to_calendar(topics, pillars, args.weeks, args.frequency, platforms, start)
analysis = analyze_calendar_balance(calendar, pillars)
if args.format == "json":
print(json.dumps({"channel": channel, "calendar": calendar, "analysis": analysis}, indent=2))
else:
print_human(calendar, analysis, channel)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Video SEO Optimizer
Optimizes video titles, descriptions, and tags for platform-specific SEO.
Analyzes existing metadata and provides optimization recommendations with
improved alternatives.
Expected JSON input with video details: title, description, tags, platform, topic
Usage:
python video_seo_optimizer.py video_data.json
python video_seo_optimizer.py video_data.json --platform youtube
python video_seo_optimizer.py video_data.json --format json
python video_seo_optimizer.py video_data.json --batch
"""
import argparse
import json
import re
import sys
import string
from collections import Counter
from typing import Any, Dict, List, Optional, Tuple
PLATFORM_LIMITS = {
"youtube": {"title_max": 100, "title_optimal": 60, "desc_max": 5000, "tags_max_chars": 500, "tags_max_count": 30},
"tiktok": {"title_max": 150, "title_optimal": 80, "desc_max": 2200, "tags_max_chars": 300, "tags_max_count": 10},
"linkedin": {"title_max": 150, "title_optimal": 70, "desc_max": 3000, "tags_max_chars": 200, "tags_max_count": 5},
}
# Common power words that drive clicks
POWER_WORDS = {
"curiosity": ["secret", "hidden", "revealed", "truth", "shocking", "surprising", "unexpected"],
"urgency": ["now", "today", "immediately", "urgent", "before", "deadline", "limited"],
"value": ["free", "ultimate", "complete", "proven", "guaranteed", "essential", "must-know"],
"numbers": ["top", "best", "worst", "first", "last", "only"],
"how_to": ["how", "why", "what", "when", "guide", "tutorial", "step", "tips", "tricks"],
}
# Stop words to identify keyword density
STOP_WORDS = {
"the", "a", "an", "is", "are", "was", "were", "be", "been", "being", "have", "has", "had",
"do", "does", "did", "will", "would", "shall", "should", "may", "might", "must", "can",
"could", "to", "of", "in", "for", "on", "with", "at", "by", "from", "as", "into", "through",
"and", "but", "or", "nor", "not", "so", "yet", "both", "either", "neither", "each", "every",
"this", "that", "these", "those", "i", "me", "my", "you", "your", "he", "she", "it", "we",
"they", "them", "his", "her", "its", "our", "their",
}
def analyze_title(title: str, platform: str) -> Dict[str, Any]:
"""Analyze title for SEO effectiveness."""
limits = PLATFORM_LIMITS.get(platform, PLATFORM_LIMITS["youtube"])
issues = []
score = 70 # Base score
# Length check
length = len(title)
if length > limits["title_max"]:
issues.append(f"Title exceeds maximum ({length}/{limits['title_max']} chars) - will be truncated")
score -= 20
elif length > limits["title_optimal"]:
issues.append(f"Title may be truncated on mobile ({length}/{limits['title_optimal']} optimal chars)")
score -= 5
elif length < 20:
issues.append("Title is very short - may lack keyword coverage")
score -= 10
# Power words
title_lower = title.lower()
found_power = []
for category, words in POWER_WORDS.items():
for word in words:
if word in title_lower:
found_power.append({"word": word, "category": category})
if found_power:
score += min(15, len(found_power) * 5)
else:
issues.append("No power words found - consider adding curiosity or value triggers")
score -= 5
# Number presence
has_number = bool(re.search(r'\d', title))
if has_number:
score += 5
else:
issues.append("No numbers in title - titles with numbers get 36% more clicks")
# ALL CAPS check
words = title.split()
caps_words = sum(1 for w in words if w.isupper() and len(w) > 1)
if caps_words > len(words) * 0.5:
issues.append("Excessive ALL CAPS - use sparingly for emphasis (1-2 words max)")
score -= 10
# Keyword front-loading
# First 5 words should contain primary topic
first_five = " ".join(words[:5]).lower()
# Question format detection
is_question = title.strip().endswith("?") or title_lower.startswith(("how", "why", "what", "when", "where"))
return {
"title": title,
"length": length,
"optimal_length": limits["title_optimal"],
"max_length": limits["title_max"],
"has_number": has_number,
"is_question": is_question,
"power_words": found_power,
"caps_words": caps_words,
"issues": issues,
"score": max(0, min(100, score)),
}
def analyze_description(description: str, platform: str, keywords: List[str] = None) -> Dict[str, Any]:
"""Analyze description for SEO effectiveness."""
limits = PLATFORM_LIMITS.get(platform, PLATFORM_LIMITS["youtube"])
issues = []
score = 70
length = len(description)
word_count = len(description.split())
if length > limits["desc_max"]:
issues.append(f"Description exceeds limit ({length}/{limits['desc_max']} chars)")
score -= 15
elif word_count < 50:
issues.append("Description is thin - aim for 150-300 words for SEO value")
score -= 10
elif word_count >= 150:
score += 10
# First 2-3 lines check (visible without expanding)
lines = description.strip().split("\n")
first_lines = " ".join(lines[:3])
if len(first_lines) < 50:
issues.append("First 2-3 lines are too short - this is the visible preview")
score -= 5
# Keyword density
if keywords:
desc_lower = description.lower()
for kw in keywords:
count = desc_lower.count(kw.lower())
if count == 0:
issues.append(f"Keyword '{kw}' not found in description")
score -= 5
elif count >= 5:
issues.append(f"Keyword '{kw}' may be over-stuffed ({count} occurrences)")
score -= 3
# Has timestamps (YouTube)
has_timestamps = bool(re.search(r'\d{1,2}:\d{2}', description))
if platform == "youtube" and not has_timestamps:
issues.append("No timestamps found - add chapter markers for longer videos")
elif has_timestamps:
score += 5
# Has links/CTA
has_links = "http" in description or "www." in description
has_cta = any(cta in description.lower() for cta in ["subscribe", "follow", "sign up", "download", "click", "link"])
if not has_cta:
issues.append("No call-to-action found in description")
return {
"length": length,
"word_count": word_count,
"has_timestamps": has_timestamps,
"has_links": has_links,
"has_cta": has_cta,
"issues": issues,
"score": max(0, min(100, score)),
}
def analyze_tags(tags: List[str], platform: str) -> Dict[str, Any]:
"""Analyze tags for SEO effectiveness."""
limits = PLATFORM_LIMITS.get(platform, PLATFORM_LIMITS["youtube"])
issues = []
score = 70
if not tags:
return {"count": 0, "issues": ["No tags provided"], "score": 20}
total_chars = sum(len(t) for t in tags)
count = len(tags)
if count > limits["tags_max_count"]:
issues.append(f"Too many tags ({count}/{limits['tags_max_count']} max)")
score -= 10
elif count < 5:
issues.append(f"Too few tags ({count}) - aim for 10-20 for YouTube")
score -= 10
if total_chars > limits["tags_max_chars"]:
issues.append(f"Total tag characters exceed limit ({total_chars}/{limits['tags_max_chars']})")
score -= 10
# Tag length analysis
short_tags = [t for t in tags if len(t.split()) == 1]
long_tail = [t for t in tags if len(t.split()) >= 3]
if not long_tail:
issues.append("No long-tail tags - add 3+ word phrases for specific search queries")
score -= 5
else:
score += 5
# Duplicate check
normalized = [t.lower().strip() for t in tags]
dupes = [t for t, c in Counter(normalized).items() if c > 1]
if dupes:
issues.append(f"Duplicate tags found: {', '.join(dupes)}")
score -= 5
return {
"count": count,
"total_chars": total_chars,
"max_chars": limits["tags_max_chars"],
"short_tags": len(short_tags),
"long_tail_tags": len(long_tail),
"duplicates": dupes,
"issues": issues,
"score": max(0, min(100, score)),
}
def optimize_video(video: Dict[str, Any], platform: str) -> Dict[str, Any]:
"""Run full optimization analysis on a video."""
title = video.get("title", "")
description = video.get("description", "")
tags = video.get("tags", [])
keywords = video.get("keywords", [])
title_analysis = analyze_title(title, platform)
desc_analysis = analyze_description(description, platform, keywords)
tags_analysis = analyze_tags(tags, platform)
# Overall score
overall_score = (
title_analysis["score"] * 0.40
+ desc_analysis["score"] * 0.35
+ tags_analysis["score"] * 0.25
)
# Aggregate recommendations
all_issues = []
all_issues.extend([{"area": "title", "issue": i} for i in title_analysis["issues"]])
all_issues.extend([{"area": "description", "issue": i} for i in desc_analysis["issues"]])
all_issues.extend([{"area": "tags", "issue": i} for i in tags_analysis["issues"]])
return {
"video_id": video.get("video_id", ""),
"title": title,
"platform": platform,
"title_analysis": title_analysis,
"description_analysis": desc_analysis,
"tags_analysis": tags_analysis,
"overall_score": round(overall_score, 1),
"total_issues": len(all_issues),
"issues": all_issues,
}
def print_human(results: List[Dict[str, Any]]) -> None:
"""Print optimization results in human-readable format."""
for result in results:
print("=" * 70)
print(f" Video SEO Analysis - {result['platform'].upper()}")
print("=" * 70)
print(f"\n Title: {result['title']}")
print(f" Overall SEO Score: {result['overall_score']}/100")
# Title
ta = result["title_analysis"]
print(f"\n --- Title Analysis (Score: {ta['score']}/100) ---")
print(f" Length: {ta['length']}/{ta['optimal_length']} optimal chars")
print(f" Has Number: {'Yes' if ta['has_number'] else 'No'}")
print(f" Question Format: {'Yes' if ta['is_question'] else 'No'}")
if ta["power_words"]:
pw = ", ".join(f"{p['word']} ({p['category']})" for p in ta["power_words"])
print(f" Power Words: {pw}")
# Description
da = result["description_analysis"]
print(f"\n --- Description Analysis (Score: {da['score']}/100) ---")
print(f" Word Count: {da['word_count']}")
print(f" Timestamps: {'Yes' if da['has_timestamps'] else 'No'}")
print(f" Links: {'Yes' if da['has_links'] else 'No'}")
print(f" CTA: {'Yes' if da['has_cta'] else 'No'}")
# Tags
tg = result["tags_analysis"]
print(f"\n --- Tags Analysis (Score: {tg['score']}/100) ---")
print(f" Tag Count: {tg['count']}")
print(f" Long-tail Tags: {tg.get('long_tail_tags', 0)}")
# Issues
if result["issues"]:
print(f"\n --- Issues ({result['total_issues']}) ---")
for item in result["issues"]:
print(f" [{item['area']:>11}] {item['issue']}")
print()
def main():
parser = argparse.ArgumentParser(
description="Optimize video titles, descriptions, and tags for platform SEO"
)
parser.add_argument("file", help="JSON file with video data")
parser.add_argument("--format", choices=["human", "json"], default="human", help="Output format")
parser.add_argument("--platform", choices=["youtube", "tiktok", "linkedin"],
default="youtube", help="Target platform (default: youtube)")
parser.add_argument("--batch", action="store_true",
help="Process multiple videos (expects 'videos' array in JSON)")
args = parser.parse_args()
with open(args.file, "r", encoding="utf-8") as f:
data = json.load(f)
if args.batch:
videos = data.get("videos", [])
else:
videos = [data]
if not videos:
print("Error: No video data found", file=sys.stderr)
sys.exit(1)
results = [optimize_video(v, args.platform) for v in videos]
if args.format == "json":
print(json.dumps({"results": results} if len(results) > 1 else results[0], indent=2))
else:
print_human(results)
if __name__ == "__main__":
main()
Related skills
FAQ
Which platforms does it support?
YouTube, TikTok, LinkedIn, and Instagram, with platform-specific length guidance.
What tools does it include?
A content planner, a thumbnail analyzer, and a video SEO optimizer, all Python scripts.