feat: v14.0.0 — 12 community-inspired skills, pm-writers profession, extend pm-cross/operations/engineering

New profession: Writers & Content Creators (pm-writers bundle, skills 156–160)
- instagram-post-downloader: Downloads Instagram images/carousels as high-res files + PDF stitch
- aeo-optimizer: Restructures articles for AI citation (AEO) — question H2s, answer capsules, trust signal audit
- thumbnail-creator: Generates brand-aligned thumbnail candidates via Gemini API with computer vision eval
- substack-notes-scraper: Scrapes Substack Notes engagement data to formatted .xlsx
- notes-humanizer: Strips AI writing patterns across 3 phases; injects genuine human signals

Extended pm-cross (+3 skills, skills 161–163):
- sycophancy-challenger: Argues against your idea first, holds position under pushback
- last-30-days-research: Multi-platform research (Reddit, X, web) with signal confidence scoring
- notebooklm-connector: Automates NotebookLM from Claude Code via Chrome extension

Extended pm-operations (+2 skills, skills 164–165):
- email-triage: Reads Gmail and surfaces only actionable emails with priority + reply starters
- morning-intelligence: 15-question interview → personalised master news brief prompt

Extended pm-engineering (+2 skills, skills 166–167):
- context-mode: Output filtering + session log for long Claude Code sessions
- claude-superpowers: Plan→Isolate→Test→Double-review framework for Claude Code

Updated: marketplace.json v14.0.0 (167 skills, 18 professions, 26 bundles)
Updated: README.md — title, badges, What's New, All 167 Skills table, install list

Credits: skills inspired by Frank & Diana Dovgopol, Gencay (LearnAIwithMe), Karen Spinner (Wondering About AI), Orel (TheIndiepreneur), Joel Salinas (Leadership in Change), Ilia Karelin (Prosper), Ashwin Francis (Cash&Cache), Nate Herk

https://claude.ai/code/session_01E4bTUWxx4Zo5rsFpad5X5B
This commit is contained in:
Mohit Aggarwal
2026-05-27 12:29:45 +00:00
parent 6bb25a8c13
commit 20eda05cc6
16 changed files with 4420 additions and 21 deletions
@@ -0,0 +1,531 @@
---
name: aeo-optimizer
description: "Optimize an article for Answer Engine Optimization (AEO) — restructuring content so AI engines like ChatGPT, Perplexity, and Claude can extract, quote, and cite it. Rewrites headings as questions, drops 50-80 word answer capsules, audits paragraph length, and flags trust signals. Use when asked to AEO-optimize, make content AI-readable, improve AI citation chances, or adapt an article for answer engines."
---
# AEO Optimizer Skill
AEO — Answer Engine Optimization — is the discipline of structuring content so that AI engines (ChatGPT, Perplexity, Claude, Gemini) can extract clean, quotable answers and confidently cite your content as a source.
Most articles are written for humans who scroll, skim, and click. AI engines don't scroll — they scan for extractable answer units. They look for short, self-contained answer blocks sitting directly beneath a clear question heading. If they can't find those, they either skip the content or paraphrase it poorly. This skill fixes that.
---
## The AEO Problem
Here is what AI engines are scanning for, and what most articles fail to provide:
| What AI engines want | What most articles deliver |
|---|---|
| H2 = a direct question ("What is X?") | H2 = a vague topic label ("About X" or "Understanding X") |
| 50-80 word answer capsule immediately under the heading | Long intro paragraphs before the actual answer |
| No links inside the answer block | Inline links that break extractability |
| ≤3 sentences per paragraph | Dense 6-8 sentence paragraphs |
| Named frameworks, original data, first-person experience | Generic statements with no attribution or specificity |
| Consistent question-answer-expand structure throughout | Inconsistent structure that varies section by section |
When an AI engine cannot cleanly extract a 50-80 word answer, it either skips the article or provides a vague paraphrase without a citation link. AEO optimization removes those barriers.
---
## Required Inputs
Claude will ask for these if not provided:
| Input | Required | Notes |
|---|---|---|
| Article content | Yes | Paste the full draft text, or provide a URL Claude can fetch |
| Target audience | No | Helps calibrate question phrasing — e.g. "beginner founders" vs "senior engineers" |
| Primary keyword or topic | No | If provided, Claude ensures H2 questions cover it directly |
| Existing URL (if published) | No | Used in the audit report to note the live page |
| Preserve exact section order | No | Defaults to yes — Claude rewrites in place, doesn't restructure |
If providing a URL instead of pasted text, Claude will fetch the page content. Note: paywalled or JavaScript-rendered articles may require manual paste.
---
## Output Structure
Claude produces two deliverables in sequence:
### Deliverable 1 — AEO-Ready Article
The full rewritten article with:
- All H2s rewritten as direct questions
- 50-80 word answer capsule inserted directly beneath each H2
- Paragraphs trimmed to ≤3 sentences where they exceeded that
- Trust signals preserved and lightly emphasized
- No links inside any answer capsule
- Original voice and structure maintained — this is an optimization, not a rewrite
**Format:**
```markdown
# [Original H1 title — unchanged unless it needs question format]
[Introduction — keep as-is or trim to ≤3 sentences. Add a "What this covers:" summary if intro is >150 words.]
## [H2 rewritten as a direct question?]
[Answer capsule — 50-80 words, no links, self-contained, answers the question completely on its own.]
[Rest of the section body — expanded explanation, examples, data, links allowed here]
## [Next H2 as a direct question?]
[Answer capsule — 50-80 words, no links]
[Section body]
```
---
### Deliverable 2 — AEO Audit Report
Structured report showing all changes made and signals identified.
**Format:**
---
## AEO Audit Report
**Article:** [Title]
**URL:** [If provided]
**Audit date:** [Today's date]
**AEO readiness score (before):** [X/10]
**AEO readiness score (after):** [X/10]
---
### Heading Rewrites
| Original H2 | Rewritten H2 | Change type |
|---|---|---|
| Understanding Content Strategy | What is content strategy and why does it matter? | Topic label → direct question |
| The Benefits of X | What are the main benefits of X? | Vague noun phrase → question |
| How We Do It at [Company] | How does [Company] approach X? | First-person → question format |
---
### Answer Capsule Placements
For each section, confirm capsule word count is within 50-80 words:
| Section | Capsule word count | Links removed from capsule | Status |
|---|---|---|---|
| What is content strategy...? | 64 words | 2 links removed | OK |
| How do you build a content calendar? | 71 words | 0 links (none were present) | OK |
| What tools do content teams use? | 58 words | 1 link removed | OK |
---
### Paragraph Length Audit
| Section | Original max paragraph (sentences) | Action taken |
|---|---|---|
| Introduction | 6 sentences | Split into 2 paragraphs |
| Section 2 body | 4 sentences | Trimmed to 3 |
| Section 4 body | 2 sentences | No change needed |
**Paragraphs flagged as too long (before optimization):** [N]
**Paragraphs within ≤3 sentences (after optimization):** [all]
---
### Trust Signal Inventory
Trust signals are the elements AI engines treat as credibility markers — original data, named frameworks, first-person experience, and specific attributions. These make AI engines more likely to cite rather than paraphrase.
| Signal type | Found in article | Example | AEO value |
|---|---|---|---|
| Original data / research | Yes | "Our analysis of 400 posts showed..." | High — cite-worthy claim |
| Named framework | Yes | "The RICE scoring model" | High — search anchor |
| First-person experience | Yes | "After running 3 content audits..." | Medium — authority signal |
| Named expert / quote | No | — | Recommend adding |
| Specific numbers / stats | Yes | "34% increase in organic traffic" | High — extractable fact |
| Date-stamped content | No | — | Recommend adding publication date |
| Case study reference | Yes | "At Acme Corp, we ran..." | High — concrete example |
**Trust signals present:** [N]
**Recommended additions:** [list any gaps]
---
### AEO Scoring Rubric
| Criterion | Before | After |
|---|---|---|
| H2s as direct questions (% of total) | [X%] | [X%] |
| Answer capsule present under each H2 | No | Yes |
| Capsules within 50-80 words | N/A | [X/N sections] |
| No links inside capsules | N/A | Yes |
| Paragraphs ≤3 sentences | [X%] | [X%] |
| Trust signals present | [N] | [N] |
| **Total score** | **[X/10]** | **[X/10]** |
---
### Recommended Next Steps
1. [Any remaining gaps — e.g. "Section 4 capsule is 88 words — trim by 10"]
2. [Structural suggestions — e.g. "Add a FAQ section at the end for high-volume PAA questions"]
3. [Missing trust signals — e.g. "Add a publication date and last-updated date for freshness signals"]
4. [Schema markup suggestion if applicable — FAQ schema, HowTo schema, etc.]
---
*End of AEO Audit Report*
---
## How Claude Should Execute This Skill
### Step 1 — Ingest the article
Accept the content as either:
- **Pasted text:** Treat as-is. Do not attempt to fetch a URL if text is pasted.
- **URL:** Fetch the page. Extract the main article body — ignore nav, sidebars, footers, and ad blocks. If the page is JavaScript-rendered and fetch returns only a shell, ask the user to paste the text instead.
Count the headings. Note the number of H2s, H3s, and H1s. This sets expectations for how many capsules will be written.
### Step 2 — Assess AEO readiness before touching anything
Before rewriting, score the article on the AEO rubric (see Deliverable 2 scoring table). This gives the user a before/after comparison and helps Claude identify where to focus effort.
Run through each criterion and note the count:
- How many H2s are already in question format? (count ones that end with "?")
- Does any section already have a 50-80 word self-contained answer block?
- What is the average and maximum paragraph length in sentences?
- How many trust signals are present? (scan for numbers, named frameworks, first-person phrases, quotes)
Record the before scores. Do not round up — be honest.
### Step 3 — Rewrite H2 headings as questions
For each H2 in the article, rewrite it as a direct question that a real person would ask an AI engine. Guidelines:
**The question must:**
- Be specific enough that the answer could stand alone as a snippet
- Use "What", "How", "Why", "When", "Which", or "Who" — not vague gerunds ("Understanding", "Exploring", "Unpacking")
- Match the search intent of the original section, not just rephrase it generically
- Be 8 words or fewer when possible (longer questions are harder for AI engines to match)
**Examples of heading transformations:**
| Before | After |
|---|---|
| Introduction to Agile | What is Agile methodology? |
| Why We Built This | Why did [Company] build [product]? |
| The Case for Async Work | Why do distributed teams choose async work? |
| Benefits | What are the main benefits of X? |
| Tools and Resources | Which tools do [audience] use for X? |
| Getting Started | How do you get started with X? |
| Common Mistakes | What mistakes do beginners make with X? |
| Our Approach | How does [Company/author] approach X? |
Do not rewrite H3s unless the user requests it. H3s can stay as labels — AI engines primarily anchor on H2s.
Do not change the H1. The H1 is the article title and SEO title — it follows different rules.
### Step 4 — Write answer capsules
For each H2, write a 50-80 word answer capsule to be inserted immediately after the heading and before any existing body text.
**Capsule rules:**
- Must be self-contained — someone reading only the heading + capsule should have a complete, useful answer
- No links of any kind inside the capsule (links break AI extractability)
- No hedging phrases ("It depends", "There are many factors") — commit to the answer
- Use the same voice and terminology as the article — do not change the author's perspective
- If the section has an existing strong first paragraph that is already 50-80 words and self-contained, use it as the capsule with minimal edits rather than writing a new one
- Count words precisely — under 50 is too thin, over 80 and AI engines may not extract it cleanly
**Capsule structure options:**
Option A — Definition then application:
```
[Concise definition of the concept in 1-2 sentences.] [How it applies in practice, with one specific example or number.] [Why it matters for the reader's situation.]
```
Option B — Direct answer then context:
```
[Direct answer to the heading question in 1 sentence.] [2-3 sentences of supporting context, specifics, or mechanism.] [Optional: one concrete example or stat.]
```
Option C — How-to opener:
```
[State the outcome in 1 sentence.] [Steps 1, 2, 3 in compressed form.] [Note on when this applies or what to watch for.]
```
Mark each capsule clearly with an HTML comment so the author knows it was added:
```html
<!-- AEO Answer Capsule — 64 words -->
[capsule text]
<!-- End AEO Capsule -->
```
### Step 5 — Audit and trim paragraph length
Scan every paragraph in the body sections (not the capsules). If a paragraph exceeds 3 sentences:
- Split it into two paragraphs at the most natural break
- Do not summarise or remove content — just add a paragraph break
- If a paragraph is a list in disguise (long run-on sentence with "and", "then", "also"), convert it to a bullet list instead
Note every change in the audit report's paragraph length table.
### Step 6 — Identify and flag trust signals
Scan the full article for trust signals. Do not add trust signals — only identify what exists and flag gaps. Trust signals are:
| Signal type | What to look for |
|---|---|
| Original data | "Our data shows", "We analysed X", "In our survey of N..." |
| Named frameworks | Any named methodology, model, or system (RICE, Jobs-to-be-Done, etc.) |
| First-person experience | "I found", "We ran", "When I built", "After testing..." |
| Specific numbers | Percentages, counts, timeframes, dollar amounts |
| Expert quotes | Direct quotes attributed to a named person |
| Case studies | Named company or project with specific outcomes |
| Publication freshness | A visible publish or update date |
Flag any category with zero signals as a gap. Include specific recommendations for what could be added (e.g. "Add a statistic to the intro — even a well-known industry stat cited correctly adds credibility").
### Step 7 — Assemble the output
Produce the two deliverables in this order:
1. First: the full AEO-ready article. Use the original markdown structure with the changes applied. Make sure capsules have the HTML comment markers.
2. Second: the AEO Audit Report, using the exact table structure from the Output Structure section above.
Separate the two deliverables with a clear horizontal rule (`---`) and a heading (`## AEO Audit Report`).
### Step 8 — Optional: FAQ section recommendation
If the article does not already have a FAQ section, and the topic has obvious high-volume PAA (People Also Ask) questions, recommend adding one. Provide 3-5 suggested FAQ questions in question format with brief capsule answers. Note that FAQ sections with proper schema markup (`FAQPage` JSON-LD) get preferential treatment in both traditional SEO and AI engine extraction.
---
## AEO Reference: What Makes a Good Answer Capsule
This section is reference material — Claude should use it when evaluating capsule quality.
**Strong capsule (62 words):**
> Content strategy is the planning and management of content to achieve specific business goals. It defines what to publish, for whom, through which channels, and how often. A strong content strategy starts with audience research, maps content to stages of the buyer journey, and includes a measurement framework. Without it, content teams produce output without direction — publishing more without knowing whether it drives outcomes.
Why it works:
- Answers the question completely in isolation
- No links
- Specific enough to be citable (mentions audience research, buyer journey, measurement framework)
- Under 80 words
**Weak capsule (48 words — too short, too vague):**
> Content strategy is important for businesses. It helps you plan what content to create. Many companies use content strategy to grow their audience. There are different approaches depending on your goals. It's a broad topic that covers many areas of marketing.
Why it fails:
- Does not complete the answer — "many areas" is not an answer
- No specifics, no named concepts
- Under 50 words
- AI engine would not cite this — it says nothing citable
---
## Quality Checks
Before marking this task complete, verify each item:
- [ ] Every H2 in the article is now a direct question ending with "?"
- [ ] Every question-format H2 has an answer capsule immediately below it (no intervening text)
- [ ] Every capsule is between 50 and 80 words — count precisely, not approximately
- [ ] No links appear inside any capsule block
- [ ] Every capsule has the HTML comment markers noting word count
- [ ] Paragraphs throughout the article body are ≤3 sentences (flag any exceptions in the report)
- [ ] The H1 title is unchanged
- [ ] H3s are unchanged (unless user requested otherwise)
- [ ] Original voice, tone, and terminology are preserved — this is optimization, not ghostwriting
- [ ] Trust signal inventory table is populated with actual examples from the text, not generic placeholders
- [ ] Gaps in trust signals are noted with specific recommendations, not just "add more data"
- [ ] Before and after AEO scores are both present in the audit report
- [ ] Heading rewrites table is complete — one row per H2
- [ ] Paragraph length audit table is complete — covers all sections
- [ ] Any FAQ section recommendation is based on real PAA-style questions for the topic, not invented ones
- [ ] Both deliverables (article + audit report) are present in the response
- [ ] Total word count of the rewritten article is within ±10% of the original (optimization, not expansion)
---
## Example Trigger Phrases
- "AEO optimize this article"
- "Make this content AI-readable"
- "Rewrite my headings as questions and add answer capsules"
- "Optimize this for ChatGPT and Perplexity to cite"
- "Run an AEO audit on this draft"
- "Make this article get picked up by AI search"
- "I want Perplexity to cite my content — can you fix this article?"
- "Turn these headings into questions and add short answer blocks"
- "Can you add answer capsules under each section?"
- "Audit this for answer engine optimization"
- "My content isn't showing up in AI answers — fix the structure"
- "AEO this" [followed by article text or URL]
- "Optimize for AI citation"
- "Make each section self-contained for AI extraction"
---
## Appendix: AEO vs SEO — Key Differences
This is useful context Claude can share with users who are unfamiliar with AEO:
| Dimension | SEO (Search Engine Optimization) | AEO (Answer Engine Optimization) |
|---|---|---|
| Target | Google's ranking algorithm | AI engine extraction models |
| Primary signal | Backlinks, authority, keyword density | Structured Q&A, answer capsule clarity |
| Content format | Long-form, comprehensive coverage | Question-first, capsule-first, then expand |
| Heading style | Keyword-rich labels ("Best Project Management Tools") | Direct questions ("What are the best project management tools?") |
| Paragraph length | Not a ranking factor | Short (≤3 sentences) is strongly preferred |
| Links in body | Important for authority passing | Links inside answer capsules break extractability |
| Trust signals | Domain authority, backlink profile | Named data, frameworks, first-person experience |
| Measurement | Organic ranking position, CTR | AI citation frequency, answer box appearances |
AEO does not replace SEO — it complements it. A well-structured article optimized for AEO will also perform better in traditional search because its structure is clearer and its headings are more specific to user intent.
---
## Appendix: Answer Capsule Templates by Content Type
Not all articles have the same kind of content. Use these capsule templates as starting points based on the section type.
### "What is X?" sections (definition)
```
[X] is [concise category or type]. It [what it does or how it works] by [mechanism or method].
[Why it exists or what problem it solves — 1 sentence.] [One concrete example or real-world application.]
```
Target: 55-70 words. Avoid starting with "X is a type of X" — give immediate signal.
### "How do you do X?" sections (how-to)
```
To [achieve outcome], [do step A], then [do step B], then [do step C].
[The most common mistake or prerequisite — 1 sentence.] [The expected result or timeframe.]
```
Target: 50-65 words. Use active verbs throughout. No links.
### "Why does X matter?" sections (rationale)
```
[X] matters because [specific reason 1] and [specific reason 2].
Without [X], [consequence — ideally quantified or concrete].
[Who this is most important for, and under what conditions.]
```
Target: 55-75 words. Specifics outperform generalities here — name numbers when they exist.
### "What are the benefits of X?" sections (list rationale)
```
The main benefits of [X] are [benefit 1], [benefit 2], and [benefit 3].
[Benefit 1] means [specific outcome]. [Benefit 2] enables [specific use case].
Together these make [X] valuable for [audience] who need [outcome].
```
Target: 60-80 words. Compress the list into prose — bullet lists inside capsules are less extractable.
### "Which X should I choose?" sections (comparison/decision)
```
Choose [Option A] when [condition A]. Choose [Option B] when [condition B].
The deciding factor is [key variable]. [One sentence on the most common mistake —
picking based on the wrong criterion.]
```
Target: 50-70 words. Decision capsules are among the highest-cited by AI engines — they answer the user's actual next question.
### "When should I X?" sections (timing/trigger)
```
[X] when [specific trigger condition], typically [timeframe or frequency].
Early signs that it's time include [signal 1] and [signal 2].
Waiting too long often results in [consequence].
```
Target: 45-65 words. Concise is especially important for timing capsules.
---
## Appendix: AEO Scoring Rubric — Detailed Criteria
Use this when producing the before/after score. Each criterion has a maximum contribution to the /10 score.
| Criterion | Max score | How to assess |
|---|---|---|
| H2s as direct questions | 2 pts | 2 = all H2s are questions; 1 = majority; 0 = few or none |
| Answer capsules present | 2 pts | 2 = every H2 section has a capsule; 1 = some sections; 0 = none |
| Capsules within 50-80 words | 1 pt | 1 = all capsules in range; 0 = any over 80 or under 50 |
| No links inside capsules | 1 pt | 1 = zero links in any capsule; 0 = any links present |
| Paragraphs ≤3 sentences | 2 pts | 2 = all paragraphs compliant; 1 = majority; 0 = widespread violations |
| Trust signals present | 2 pts | 2 = 3+ trust signal types; 1 = 1-2 types; 0 = none |
**Score interpretation:**
- 8-10: Strong AEO readiness — well-positioned for AI citation
- 5-7: Partial — likely extracted occasionally but inconsistently
- 0-4: Low readiness — AI engines will paraphrase at best, skip at worst
A typical unoptimized article scores 2-4. A well-structured but unoptimized thought leadership piece might score 4-6. After this skill runs, target 8+.
---
## Appendix: How Different AI Engines Extract Content
Understanding how each engine works helps explain the rules behind the skill.
### ChatGPT (GPT-4 and later) / Bing
Retrieval-augmented generation with Bing Search integration. When a user asks a question, Bing retrieves pages, then GPT extracts passages. It tends to extract the first plausible answer-shaped block it finds in the page — meaning the capsule directly under the H2 is almost always what gets quoted. It prefers prose over lists for citations (though it reads lists fine).
**Implication:** Get the capsule under the question-format H2 right. The rest of the section body is bonus context.
### Perplexity
Explicitly designed for sourced Q&A. It retrieves 5-10 pages per query and extracts from all of them simultaneously. It shows citations with numbered footnotes. It strongly prefers content that is:
- Clearly attributed (author name or publication byline visible)
- Recently published or updated (freshness signal)
- Structured around the question being asked (heading match)
**Implication:** Trust signals (author, date) and heading-to-question matching are especially important for Perplexity. Capsules that include specific numbers or named frameworks are more likely to be footnoted.
### Claude (Anthropic)
Claude with web search capability (Claude.ai or API with tools) retrieves pages and synthesises across them. Claude prioritises self-contained, complete answers and tends to directly quote capsules that are within the 50-80 word range. Claude is less likely to quote incomplete paragraphs that trail off or rely on surrounding context.
**Implication:** The self-contained requirement is especially important for Claude citation. If the capsule requires reading the surrounding sentences to make sense, Claude will paraphrase instead of quote.
### Google Gemini (AI Overviews)
Integrated into Google Search. Generates AI Overviews for informational queries. Extracts from indexed pages, with preference for pages that already rank well (so SEO and AEO reinforce each other here). Tends to extract bulleted lists and numbered steps for how-to content; extracts definition capsules for "what is" queries.
**Implication:** For Gemini AI Overviews, structured how-to content with numbered steps in the capsule performs well. Definition capsules should include the category/type as the first word.
---
## Appendix: Content Types That Benefit Most from AEO
Not all content benefits equally. Use this to set expectations with the user about where AEO investment pays off most.
| Content type | AEO benefit | Reason |
|---|---|---|
| Glossary or definition articles | Very high | AI engines are constantly answering "what is X?" queries |
| How-to guides and tutorials | Very high | Step-by-step content is a primary retrieval target |
| Comparison articles ("X vs Y") | High | Decision queries are common AI engine inputs |
| FAQ pages | High | Already in question format — just needs capsule discipline |
| Research roundups with original data | High | Named statistics are citation anchors |
| Thought leadership / opinion pieces | Medium | Opinion is less extractable; add definition and how-to sections |
| News and timely content | Medium | AI engines prefer evergreen; but breaking news gets citation bursts |
| Case studies | Medium | Specific outcomes are extractable; company-specific context less so |
| Creative writing / narrative | Low | Not structured for extraction; AEO rules don't apply |
| Product pages / landing pages | Low | Conversion-focused pages are rarely cited by AI engines |
---
*Originally created by Gencay (LearnAIwithMe) — adapted and extended for this library.*
@@ -0,0 +1,665 @@
---
name: instagram-post-downloader
description: "Download Instagram posts — single images or full carousels — directly from a URL. Fetches high-resolution files from Instagram's CDN, saves them into a named folder, and stitches carousel slides into a single PDF. Supports batch downloading of multiple URLs at once. Use when asked to download, save, or archive an Instagram post, reel thumbnail, or carousel."
---
# Instagram Post Downloader Skill
Downloads Instagram posts at full resolution from Instagram's CDN — no screenshots, no compression. Handles single images, carousels (multi-slide posts), and Reel cover images. For carousels, produces individual slide files plus a single stitched PDF. Supports batch URLs in one run.
---
## PREREQUISITE — Domain Allowlist
Before this skill can fetch any media, you must add Instagram's CDN domain to Claude Code's allowlist:
**Settings → Capabilities → Domain allowlist → Add:**
```
*.cdninstagram.com
```
Without this, all CDN fetch calls will be blocked. If you see a permission error when Claude attempts a fetch to `cdninstagram.com`, this is the fix.
---
## Required Inputs
Claude will ask for these if not provided upfront:
| Input | Required | Notes |
|---|---|---|
| Instagram post URL(s) | Yes | One per line, or comma-separated. `https://www.instagram.com/p/XXXX/` or `https://www.instagram.com/reel/XXXX/` format |
| Output directory | No | Defaults to `./instagram-downloads/` in the current working directory |
| PDF stitch for carousels | No | Defaults to **yes** — produces `carousel.pdf` alongside individual slides |
| File naming prefix | No | Optional prefix added before slide filenames, e.g. `brand_``brand_slide_01.jpg` |
**Batch input example:**
```
https://www.instagram.com/p/ABC123/
https://www.instagram.com/p/DEF456/
https://www.instagram.com/p/GHI789/
```
---
## Output Structure
For each URL processed, Claude creates a folder named after the post caption (first 40 characters, sanitised — spaces become underscores, special characters stripped). If no caption is available, the folder is named after the post shortcode.
### Single image post
```
instagram-downloads/
└── this_is_the_caption_first_40_chars/
├── image.jpg
└── metadata.txt
```
### Carousel post
```
instagram-downloads/
└── carousel_caption_first_40_chars/
├── slide_01.jpg
├── slide_02.jpg
├── slide_03.jpg
├── slide_04.jpg
├── carousel.pdf ← all slides stitched in order
└── metadata.txt
```
### Batch run (3 URLs)
```
instagram-downloads/
├── first_post_caption_sanitised/
│ ├── image.jpg
│ └── metadata.txt
├── second_post_carousel_caption/
│ ├── slide_01.jpg
│ ├── slide_02.jpg
│ ├── carousel.pdf
│ └── metadata.txt
└── third_post_caption_here/
├── image.jpg
└── metadata.txt
```
### metadata.txt format
```
Post URL: https://www.instagram.com/p/XXXX/
Shortcode: XXXX
Type: carousel | single_image | reel
Slide count: 4 (carousel only)
Caption: [full caption text]
Username: @username
Fetched at: 2026-05-27T14:32:00Z
CDN URLs:
slide_01.jpg https://scontent.cdninstagram.com/v/...
slide_02.jpg https://scontent.cdninstagram.com/v/...
```
### Completion summary (printed to terminal)
```
Instagram Post Downloader — Batch Complete
==========================================
URLs processed: 3
Posts saved: 3
Total files: 11 (9 images + 2 PDFs)
Skipped: 0
Output dir: /Users/you/project/instagram-downloads/
Results:
✓ this_is_the_caption_first_40_chars/ 1 image
✓ carousel_caption_first_40_chars/ 4 slides → carousel.pdf
✓ third_post_caption_here/ 1 image
```
---
## How Claude Should Execute This Skill
### Step 1 — Collect and validate inputs
1. Accept the URL(s) from the user. If the user pastes a comma-separated list, split on commas. If they paste one per line, split on newlines.
2. Validate each URL matches `instagram.com/p/`, `instagram.com/reel/`, or `instagram.com/tv/`. Flag malformed URLs before proceeding.
3. Confirm the output directory. If none provided, use `./instagram-downloads/` and tell the user.
4. Ask about PDF stitching preference only if the user hasn't said either way. Default is yes.
### Step 2 — For each URL: fetch the post page
Fetch the Instagram post page HTML:
```
GET https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
```
Instagram frequently changes its API surface. Use this fallback chain in order:
**Attempt A — JSON endpoint:**
```
https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis
```
Parse the JSON response. Look for `graphql.shortcode_media` or `data.shortcode_media`.
**Attempt B — Embed page (most reliable):**
```
https://www.instagram.com/p/{shortcode}/embed/captioned/
```
Fetch this page's HTML and extract `og:image` meta tags and any `window.__additionalDataLoaded` or `window.__StaticData` JSON blobs embedded in `<script>` tags.
**Attempt C — oEmbed endpoint:**
```
https://api.instagram.com/oembed/?url=https://www.instagram.com/p/{shortcode}/&omitscript=true
```
This returns `thumbnail_url` — useful for single images, but only gives the first frame for carousels.
**Headers to include on all requests:**
```
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
Accept-Language: en-US,en;q=0.9
Accept: text/html,application/xhtml+xml,application/json
```
### Step 3 — Extract CDN image URLs
From the fetched data, extract all high-resolution CDN URLs. Instagram CDN URLs follow these patterns:
```
https://scontent.cdninstagram.com/v/...jpg?...
https://scontent-lax3-1.cdninstagram.com/v/...jpg?...
https://instagram.fXXX1-1.fbcdn.net/v/...jpg?...
```
**For single image posts:**
- Extract the single `display_url` or the largest `display_resources` entry (pick the one with the highest `config_width`).
**For carousel posts:**
- Look for `edge_sidecar_to_children.edges[]` in the JSON. Each edge has its own `node.display_url` and `node.display_resources[]`.
- Iterate all edges in order. This determines slide numbering.
- Pick the highest-resolution variant from each slide's `display_resources` array.
**For Reels:**
- The cover image is extractable the same way as a single image.
- The video file itself requires a third-party tool (see Bonus section).
**If JSON extraction fails**, fall back to scraping `<meta property="og:image">` tags from the page HTML — this gives at least one image URL (the first slide or only image).
### Step 4 — Sanitise folder name
Build the folder name from the post caption:
1. Take the first 40 characters of the caption.
2. Strip all characters that are not alphanumeric, spaces, or hyphens.
3. Replace spaces and hyphens with underscores.
4. Lowercase the result.
5. Strip leading/trailing underscores.
6. If the result is empty (e.g. caption was all emoji), use the post shortcode instead.
```python
import re
def sanitise_folder_name(caption: str, shortcode: str) -> str:
truncated = caption[:40]
cleaned = re.sub(r'[^a-zA-Z0-9 \-]', '', truncated)
underscored = re.sub(r'[\s\-]+', '_', cleaned).strip('_').lower()
return underscored if underscored else shortcode
```
### Step 5 — Create output folder structure
```python
import os
base_dir = "./instagram-downloads"
folder_name = sanitise_folder_name(caption, shortcode)
post_dir = os.path.join(base_dir, folder_name)
os.makedirs(post_dir, exist_ok=True)
```
If a folder with that name already exists (e.g. running the same URL twice), append the shortcode to avoid collision: `folder_name_SHORTCODE`.
### Step 6 — Download each image file
For each CDN URL, download the file with a streaming GET request:
```python
import requests
def download_file(url: str, dest_path: str) -> bool:
headers = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
"Referer": "https://www.instagram.com/",
}
response = requests.get(url, headers=headers, stream=True, timeout=30)
response.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
return True
```
Name files:
- Single image: `image.jpg`
- Carousel slides: `slide_01.jpg`, `slide_02.jpg`, ... (zero-padded to 2 digits, or 3 digits if >99 slides)
Detect file format from the `Content-Type` header or URL extension. Instagram serves JPEG for photos and may serve WebP in some cases — preserve the actual extension.
### Step 7 — Stitch carousel PDF (if applicable)
After all slides are downloaded, stitch them into a single PDF using Pillow:
```python
from PIL import Image
def stitch_to_pdf(image_paths: list[str], output_path: str) -> None:
"""
Combine a list of image files into a single multi-page PDF.
Each image becomes one page. Page size matches the image dimensions.
"""
images = []
for path in sorted(image_paths): # sort ensures slide_01, slide_02, ... order
img = Image.open(path).convert("RGB")
images.append(img)
if not images:
return
first = images[0]
rest = images[1:]
first.save(
output_path,
format="PDF",
save_all=True,
append_images=rest,
resolution=150.0,
)
```
Save as `carousel.pdf` in the post folder. If Pillow is not installed, run `pip install Pillow` first — or instruct the user to do so.
**Dependency check at start of skill:**
```python
try:
from PIL import Image
except ImportError:
print("Pillow not installed. Run: pip install Pillow")
print("PDF stitching will be skipped. Individual slides will still be downloaded.")
skip_pdf = True
```
### Step 8 — Write metadata.txt
Write a `metadata.txt` file into the post folder with all extracted metadata:
```python
from datetime import datetime, timezone
def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_urls):
lines = [
f"Post URL: {post_url}",
f"Shortcode: {shortcode}",
f"Type: {post_type}",
]
if post_type == "carousel":
lines.append(f"Slide count: {len(cdn_urls)}")
lines += [
f"Caption: {caption}",
f"Username: @{username}",
f"Fetched at: {datetime.now(timezone.utc).isoformat()}",
"CDN URLs:",
]
for filename, url in cdn_urls.items():
lines.append(f" {filename:<16} {url}")
with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
```
### Step 9 — Print completion summary
After processing all URLs, print the summary table to the terminal (format shown in Output Structure section above). Include:
- Total URLs attempted
- Posts successfully saved
- Total files written (images + PDFs separately)
- Any URLs that were skipped and the reason
### Step 10 — Handle errors gracefully
| Error scenario | Action |
|---|---|
| URL is not an Instagram URL | Skip with message: "Skipped — not an Instagram URL: [url]" |
| Post is private or requires login | Skip with message: "Skipped — post is private or login required: [url]" |
| CDN fetch returns 403/404 | Try alternate CDN URL if available; if none, skip slide and note in metadata |
| Pillow not installed | Skip PDF stitching, save slides only, note in summary |
| Network timeout | Retry once after 5 seconds; if still failing, skip and log |
| Folder name collision | Append shortcode suffix to folder name |
| Rate limiting (429) | Wait 10 seconds and retry; log if retry also fails |
---
## Bonus — Downloading Instagram Reels (Video)
This skill covers images and carousel PDFs. For Reels video files, Claude Code cannot download video directly without a third-party tool, because Instagram's video CDN uses signed URLs and additional auth tokens.
**Recommended approach for Reels:**
Use `yt-dlp`, a maintained open-source tool:
```bash
# Install
pip install yt-dlp
# Download a Reel
yt-dlp "https://www.instagram.com/reel/XXXX/" -o "%(title)s.%(ext)s"
# Download to a specific folder
yt-dlp "https://www.instagram.com/reel/XXXX/" \
-o "./instagram-downloads/%(uploader)s_%(id)s.%(ext)s"
# Download best quality
yt-dlp -f "bestvideo+bestaudio" "https://www.instagram.com/reel/XXXX/"
```
Claude can run this command via Bash if the user asks. `yt-dlp` handles the auth token extraction automatically for public Reels.
---
## Full Script Template
Claude should offer to write this as a standalone script (`instagram_downloader.py`) that the user can run independently:
```python
#!/usr/bin/env python3
"""
Instagram Post Downloader
Fetches high-res images from public Instagram posts and carousels.
Requires: pip install requests Pillow
"""
import os
import re
import sys
import json
import time
import requests
from datetime import datetime, timezone
from pathlib import Path
try:
from PIL import Image
PILLOW_AVAILABLE = True
except ImportError:
PILLOW_AVAILABLE = False
print("Warning: Pillow not installed. PDF stitching disabled. Run: pip install Pillow")
HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "en-US,en;q=0.9",
"Referer": "https://www.instagram.com/",
}
def extract_shortcode(url: str) -> str:
match = re.search(r"instagram\.com/(?:p|reel|tv)/([A-Za-z0-9_-]+)", url)
if not match:
raise ValueError(f"Cannot extract shortcode from URL: {url}")
return match.group(1)
def fetch_post_data(shortcode: str) -> dict:
"""Try multiple endpoints to get post JSON data."""
# Attempt A: JSON endpoint
try:
url = f"https://www.instagram.com/p/{shortcode}/?__a=1&__d=dis"
r = requests.get(url, headers=HEADERS, timeout=15)
if r.status_code == 200:
data = r.json()
media = (data.get("graphql", {}).get("shortcode_media") or
data.get("data", {}).get("shortcode_media"))
if media:
return media
except Exception:
pass
# Attempt B: Embed page
try:
url = f"https://www.instagram.com/p/{shortcode}/embed/captioned/"
r = requests.get(url, headers=HEADERS, timeout=15)
html = r.text
# Look for JSON blob in script tags
matches = re.findall(r'window\.__additionalDataLoaded\([^,]+,(\{.+?\})\);', html)
for blob in matches:
try:
data = json.loads(blob)
media = (data.get("graphql", {}).get("shortcode_media") or
data.get("data", {}).get("shortcode_media"))
if media:
return media
except json.JSONDecodeError:
continue
except Exception:
pass
return {}
def get_cdn_urls(media: dict) -> list[tuple[str, str]]:
"""Return list of (filename, cdn_url) tuples."""
results = []
media_type = media.get("__typename", "")
if media_type == "GraphSidecar":
edges = media.get("edge_sidecar_to_children", {}).get("edges", [])
for i, edge in enumerate(edges, start=1):
node = edge.get("node", {})
resources = node.get("display_resources", [])
url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
if resources else node.get("display_url", ""))
if url:
ext = "jpg" if "jpg" in url.lower() else "webp"
filename = f"slide_{i:02d}.{ext}"
results.append((filename, url))
else:
resources = media.get("display_resources", [])
url = (max(resources, key=lambda r: r.get("config_width", 0)).get("src")
if resources else media.get("display_url", ""))
if url:
ext = "jpg" if "jpg" in url.lower() else "webp"
results.append((f"image.{ext}", url))
return results
def sanitise_folder_name(caption: str, shortcode: str) -> str:
truncated = caption[:40] if caption else ""
cleaned = re.sub(r"[^a-zA-Z0-9 \-]", "", truncated)
underscored = re.sub(r"[\s\-]+", "_", cleaned).strip("_").lower()
return underscored if underscored else shortcode
def download_file(url: str, dest_path: str) -> bool:
r = requests.get(url, headers=HEADERS, stream=True, timeout=30)
r.raise_for_status()
with open(dest_path, "wb") as f:
for chunk in r.iter_content(chunk_size=8192):
f.write(chunk)
return True
def stitch_pdf(image_paths: list[str], output_path: str) -> None:
if not PILLOW_AVAILABLE:
return
images = [Image.open(p).convert("RGB") for p in sorted(image_paths)]
if images:
images[0].save(output_path, format="PDF", save_all=True,
append_images=images[1:], resolution=150.0)
def process_url(post_url: str, base_dir: str, stitch_pdf_flag: bool) -> dict:
result = {"url": post_url, "status": "ok", "files": [], "error": None}
try:
shortcode = extract_shortcode(post_url)
media = fetch_post_data(shortcode)
caption = ""
username = ""
if media:
caption_edges = media.get("edge_media_to_caption", {}).get("edges", [])
caption = caption_edges[0]["node"]["text"] if caption_edges else ""
owner = media.get("owner", {})
username = owner.get("username", "")
folder_name = sanitise_folder_name(caption, shortcode)
post_dir = os.path.join(base_dir, folder_name)
if os.path.exists(post_dir):
post_dir = f"{post_dir}_{shortcode}"
os.makedirs(post_dir, exist_ok=True)
cdn_urls = get_cdn_urls(media) if media else []
if not cdn_urls:
# Fallback: oEmbed
oembed_url = f"https://api.instagram.com/oembed/?url={post_url}&omitscript=true"
r = requests.get(oembed_url, headers=HEADERS, timeout=10)
if r.status_code == 200:
thumb = r.json().get("thumbnail_url", "")
if thumb:
cdn_urls = [("image.jpg", thumb)]
username = r.json().get("author_name", "")
downloaded_paths = []
cdn_map = {}
for filename, url in cdn_urls:
dest = os.path.join(post_dir, filename)
download_file(url, dest)
downloaded_paths.append(dest)
cdn_map[filename] = url
result["files"].append(filename)
if stitch_pdf_flag and len(downloaded_paths) > 1 and PILLOW_AVAILABLE:
pdf_path = os.path.join(post_dir, "carousel.pdf")
stitch_pdf(downloaded_paths, pdf_path)
result["files"].append("carousel.pdf")
post_type = "carousel" if len(cdn_urls) > 1 else "single_image"
write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map)
result["files"].append("metadata.txt")
except Exception as e:
result["status"] = "error"
result["error"] = str(e)
return result
def write_metadata(post_dir, post_url, shortcode, post_type, caption, username, cdn_map):
lines = [
f"Post URL: {post_url}",
f"Shortcode: {shortcode}",
f"Type: {post_type}",
]
if post_type == "carousel":
lines.append(f"Slide count: {len([k for k in cdn_map if 'slide' in k])}")
lines += [
f"Caption: {caption}",
f"Username: @{username}",
f"Fetched at: {datetime.now(timezone.utc).isoformat()}",
"CDN URLs:",
]
for fn, url in cdn_map.items():
lines.append(f" {fn:<18} {url}")
with open(os.path.join(post_dir, "metadata.txt"), "w", encoding="utf-8") as f:
f.write("\n".join(lines) + "\n")
def main(urls: list[str], base_dir: str = "./instagram-downloads", stitch: bool = True):
os.makedirs(base_dir, exist_ok=True)
results = []
for url in urls:
url = url.strip()
if not url:
continue
print(f"Processing: {url}")
r = process_url(url, base_dir, stitch)
results.append(r)
time.sleep(1) # polite delay between requests
# Summary
ok = [r for r in results if r["status"] == "ok"]
err = [r for r in results if r["status"] == "error"]
total_files = sum(len(r["files"]) for r in ok)
print("\nInstagram Post Downloader — Batch Complete")
print("==========================================")
print(f"URLs processed: {len(results)}")
print(f"Posts saved: {len(ok)}")
print(f"Total files: {total_files}")
print(f"Errors: {len(err)}")
print(f"Output dir: {os.path.abspath(base_dir)}\n")
for r in results:
if r["status"] == "ok":
print(f" OK {r['url']}")
else:
print(f" ERR {r['url']}{r['error']}")
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python instagram_downloader.py <url1> [url2] ...")
sys.exit(1)
main(sys.argv[1:])
```
---
## Quality Checks
Before marking the task complete, verify each item:
- [ ] Domain allowlist confirmed — `*.cdninstagram.com` is added before any fetch attempts
- [ ] All provided URLs validated as Instagram URLs before processing begins
- [ ] CDN URLs are the highest-resolution variants available (largest `config_width` selected)
- [ ] Folder name is sanitised — no special characters, no spaces, max 40 chars from caption
- [ ] Folder collision handled — shortcode appended if folder already exists
- [ ] Carousel slides numbered sequentially with zero-padding (`slide_01`, `slide_02`, ...)
- [ ] PDF includes all slides in correct order (not alphabetical — by slide index)
- [ ] metadata.txt written to every post folder, including full CDN URLs
- [ ] Pillow dependency checked at startup — graceful fallback if not available
- [ ] Batch completion summary printed with file counts and any errors
- [ ] Private post errors caught and reported — not silently skipped
- [ ] Rate limiting handled — at least 1 second delay between requests
- [ ] No credential or cookie storage — skill operates on public posts only
---
## Example Trigger Phrases
- "Download this Instagram post for me: https://www.instagram.com/p/ABC123/"
- "Save that carousel to my downloads folder"
- "Can you grab all the slides from this Instagram post and make a PDF?"
- "Download these 5 Instagram posts" [followed by list of URLs]
- "Archive this IG post before it gets deleted"
- "I need the full-res images from this carousel"
- "Download the images from this Instagram URL and stitch them into a PDF"
- "Batch download these Instagram posts" [followed by URLs]
- "Save the slides from this Instagram carousel as individual JPEGs"
- "Get me the high-res version of this Instagram image"
---
## Notes on Instagram's Anti-Scraping Measures
Instagram actively changes its page structure and API endpoints. If all three fetch attempts fail:
1. The embed page method (`/embed/captioned/`) is historically the most stable — start there.
2. CDN URLs expire. Download immediately after fetching — do not store URLs and download later.
3. Instagram may return a login wall for some posts even if they're technically public. If this happens, the skill cannot proceed without authentication (which is out of scope).
4. If Instagram returns a 429, wait 1030 seconds before retrying. Reduce batch size for large lists.
This skill is designed for public posts only. It does not support login, sessions, or private content.
---
*Originally inspired by a skill from Frank and Diana Dovgopol (Write, Prompt, Scale) — adapted and extended for this library.*
@@ -0,0 +1,166 @@
---
name: notes-humanizer
description: Strips AI writing patterns from text and rewrites it to sound genuinely human — not by softening it, but by removing statistical defaults and injecting the specific signals that human writers produce.
---
# Notes Humanizer
"Humanize this" prompts don't work because they don't know what to remove. AI text has specific, identifiable defaults — em dashes used as parenthetical substitutes, rule-of-three lists where all items have identical rhythm, sentences that hover between 15 and 20 words. Fix those defaults, add the signals human writers actually produce, and the text stops reading as synthetic. This skill does that systematically, in two phases, and shows you exactly what changed and why.
> Credit: Originally created by Orel (TheIndiepreneur) — adapted and extended for this library.
---
## Required Inputs
| Input | Format | Notes |
|---|---|---|
| Text to humanize | Paste directly into the chat | Any length. Works on paragraphs, full articles, social posts, emails. |
No other inputs required. Claude will not ask clarifying questions before starting — it works with what's given.
---
## Output Structure
### Section 1: What Was Found
A plain-language audit of the AI patterns detected in the original text, before any rewriting:
```
PATTERNS DETECTED
─────────────────
Em dashes used as parenthetical substitutes: 3
Filler openers ("Let's dive in", "It's worth noting", etc.): 2
Rule-of-three lists with identical rhythm: 1
Sentence length variance: low (avg 17 words, range 1421)
Hedging qualifiers: 4
Passive constructions where active is cleaner: 2
```
### Section 2: Side-by-Side Comparison
| Original | Rewritten |
|---|---|
| [original paragraph] | [rewritten paragraph] |
(One row per paragraph or logical block. Short texts get the full comparison in one table. Long texts get the table collapsed to changed sections only, with unchanged sections noted.)
### Section 3: Change Log
Every specific change made, with the reason:
```
CHANGES MADE
────────────────────────────────────────────────
1. Removed em dash in "success — and it shows"
→ Rewritten as "success (and it shows)"
Why: em dash here is a parenthetical substitute, not a genuine pause
2. Deleted "It's worth noting that"
Why: pure filler — the sentence is stronger without it
3. Broke rule-of-three list "X, Y, and Z"
→ "X and Y. Z is different — [expanded thought]"
Why: all three items had identical rhythm; broke the pattern
4. Added short sentence: "That's the problem."
Why: needed a sub-8-word sentence to vary rhythm
5. Added sentence starting with "But"
Why: human writers do this; AI avoids it as a statistical default
6. Added specific example: [detail added]
Why: the original made an abstract claim with no grounding detail
7. Added aside: "(I've watched this fail three times in a row)"
Why: breaks fourth wall slightly; signals genuine perspective
```
### Section 4: Clean Output
The full rewritten text, ready to copy and paste — no annotations, no formatting artifacts.
```
[Full rewritten text here]
```
---
## Instructions for Claude
### Phase 1: Audit
Read the full text before making any changes. Identify and count every instance of these patterns:
**Patterns to remove or rewrite:**
| Pattern | Action |
|---|---|
| Em dash used as parenthetical substitute (`word — word` where a comma or parenthesis would work) | Replace with parentheses or rewrite the clause |
| "Let's dive in" | Delete or replace with a direct first sentence |
| "In conclusion" | Delete or rewrite as a genuine closing thought |
| "It's worth noting that" | Delete — the sentence stands without it |
| "At its core" | Delete or rewrite |
| "Game-changer" | Replace with what the thing actually changes |
| "Delve" | Replace with look, dig, explore — or rewrite the sentence |
| "Navigate" used metaphorically for non-navigation tasks | Replace with a direct verb |
| Rule-of-three lists where all three items have identical grammatical structure and similar word count | Break the third item out as its own sentence or expand it |
| Sentences where every sentence in a paragraph falls in the 1422 word range | Deliberately add one very short sentence and one longer one |
| "Needless to say" | Delete |
| "It's important to note that" | Delete |
| Passive constructions where the active form is more direct | Flip to active |
Do not remove every em dash — only the ones used as parenthetical substitutes. Do not remove all hedging — only empty hedging that adds no information.
### Phase 2: Inject
After stripping patterns, add the following signals. Each one should emerge from the actual content — don't add generic filler:
1. **One genuine opinion or take.** The author appears to actually believe something specific. State it without hedging. ("This approach works, and I think most people underestimate how rarely the alternative does.")
2. **One specific detail, example, or number.** Ground the most abstract claim in the text with something concrete. If the text says "this happens frequently," add a real or illustrative number. If it says "many companies do this," name the type of company.
3. **One aside or parenthetical thought that breaks the fourth wall slightly.** This is the signal most synthetic text lacks — the writer momentarily steps out of the formal argument to say something human. ("(I've seen this specific mistake made by people who absolutely should have known better.)")
4. **At least one sentence under 8 words.** Make it land on a point, not a transition.
5. **One sentence that starts with "And" or "But."** Place it where the rhythm earns it, not randomly.
### Phase 3: Report
Present the output in the four-section structure defined above. The change log must list every individual change — not categories of change, but specific instances. If you changed three em dashes, list all three separately.
### Handling edge cases
- **If the text is already mostly clean:** Report what you found (or didn't find), make the few remaining changes, and note explicitly that the original was close. Don't invent problems.
- **If the text is very short (under 100 words):** Skip the comparison table. Show original, then rewritten, then change log.
- **If the text is over 1,500 words:** Process the full text but collapse the comparison table to changed sections only.
---
## Quality Checks
- [ ] Audit was completed before rewriting (patterns counted, not just detected)
- [ ] Every removed pattern is listed in the change log with a specific reason
- [ ] Em dashes were assessed individually — only parenthetical-substitute uses were removed
- [ ] Rule-of-three lists: the rhythm was actually checked, not just the fact that there were three items
- [ ] At least one sentence under 8 words was added (or was already present)
- [ ] At least one sentence starts with "And" or "But" in the final text
- [ ] The specific detail or example added connects to an actual claim in the text, not floated in generically
- [ ] The aside breaks the fourth wall slightly without being forced or cutesy
- [ ] The change log lists specific instances, not categories
- [ ] The clean output section has no annotations or formatting artifacts — ready to paste
- [ ] If the original was already clean, that was stated explicitly rather than changes invented
---
## Example Trigger Phrases
- "Humanize this text: [paste]"
- "Use the notes-humanizer skill on this draft"
- "This reads like ChatGPT wrote it — fix it: [paste]"
- "Strip the AI out of this and make it sound like a real person wrote it"
- "Run the humanizer on this LinkedIn post: [paste]"
- "This has too many em dashes and rule-of-three lists — clean it up: [paste]"
- "Make this email sound less robotic: [paste]"
@@ -0,0 +1,176 @@
---
name: substack-notes-scraper
description: Scrapes a Substack Notes page and exports engagement data (likes, comments, restacks) to a formatted .xlsx file with conditional formatting and summary stats.
---
# Substack Notes Scraper
Substack has no public API for Notes analytics. You can't see likes, comments, and restacks in one place without scrolling through your feed manually. This skill scrapes the rendered Notes page, filters to only your original content, and exports everything to a spreadsheet you can actually analyze.
> Credit: Originally created by a Substack newsletter author — adapted and extended for this library.
---
## Required Inputs
| Input | Format | Example |
|---|---|---|
| Notes URL | Full URL to the Notes tab | `https://substack.com/@handle/notes` |
| Author handle or name | Exact handle or display name | `@handle` or `Jane Smith` |
| Date range | Plain English or explicit range | `last 30 days` or `Jan 2026 Mar 2026` |
Claude will ask for these if not provided upfront.
---
## Output Structure
### File
```
substack-notes-[handle]-[YYYY-MM-DD].xlsx
```
### Sheet: "Notes Data"
| Column | Description |
|---|---|
| Date | Publication date (YYYY-MM-DD) |
| Text Preview | First 200 characters of the note |
| Full Text | Complete note text |
| Likes | Like count at time of scrape |
| Comments | Comment count |
| Restacks | Restack count |
| Total Engagement | Likes + Comments + Restacks |
| Link | Direct URL to the note |
| Note Type | `original` or `restack` |
**Formatting applied:**
- Row 1: frozen header row
- Auto-filter enabled on all columns
- Top 20% by Likes column: highlighted yellow (`#FFF2CC`)
- Column widths: auto-fit to content, min 12, max 60
### Sheet: "Summary"
```
Scrape Date: [YYYY-MM-DD HH:MM UTC]
Author: [handle]
Date Range: [start] [end]
Total Notes: [n]
Original Notes: [n]
Restacks Filtered: [n]
Avg Likes: [n.n]
Avg Comments: [n.n]
Avg Restacks: [n.n]
Avg Total Eng: [n.n]
Best Note (Likes): [date] — [first 80 chars] — [n] likes
Best Note (Eng): [date] — [first 80 chars] — [n] total engagement
```
---
## Instructions for Claude
### Step 1: Validate inputs
Confirm the three required inputs are present. If any are missing, ask before proceeding. Parse the date range into a concrete start date and end date (convert relative ranges like "last 30 days" to explicit dates using today's date).
### Step 2: Fetch the Notes page
Use `WebFetch` to load the Notes URL. Substack Notes pages are JavaScript-rendered — request the full rendered HTML. If WebFetch returns a skeleton page without note content, note this in your response and ask the user to paste the page HTML manually or confirm browser access is available.
### Step 3: Paginate through all notes in the date window
Substack Notes load incrementally. Repeat fetching or scrolling until either:
- A note's date falls outside the target date range (stop loading more), or
- No new content loads on the next request.
Rate-limit: wait 2 seconds between each paginated request. Do not hammer the endpoint.
### Step 4: Parse each note
For every note element found on the page, extract:
- **Date**: the timestamp on the note (convert to YYYY-MM-DD)
- **Author**: the display name or handle shown on the note
- **Full text**: complete body text, stripping HTML tags
- **Text preview**: first 200 characters of full text
- **Likes count**: the number shown on the like/heart counter
- **Comments count**: the number shown on the comment counter
- **Restacks count**: the number shown on the restack counter
- **Link**: the direct permalink to the note
- **Note type**: `original` if the author matches the specified author; `restack` if it belongs to someone else
### Step 5: Filter
Keep ALL rows in the data (restacks included as rows with `Note Type = restack`). The Summary sheet stats should count only `original` notes. Mark restacks clearly so the user can filter them out themselves in Excel if preferred.
Apply date filter: exclude any note outside the specified date range.
### Step 6: Calculate Total Engagement
For each row: `Total Engagement = Likes + Comments + Restacks`
### Step 7: Identify top 20% by Likes
Sort original notes by Likes descending. Mark the top 20% (round up) for conditional formatting. These rows will be highlighted yellow in the output file.
### Step 8: Build the .xlsx file
Use Python with `openpyxl` to generate the file. Structure:
```python
# Required libraries
import openpyxl
from openpyxl.styles import PatternFill, Font, Alignment
from openpyxl.utils import get_column_letter
from datetime import datetime
# Sheet 1: Notes Data
# - Write header row, bold, freeze row 1
# - Write all data rows
# - Apply auto-filter: ws.auto_filter.ref = ws.dimensions
# - Apply yellow fill to top-20% rows by likes
# - Auto-size columns (iterate cells to find max length)
# Sheet 2: Summary
# - Write summary stats as key-value pairs, no table format
```
Name the file `substack-notes-[handle]-[YYYY-MM-DD].xlsx` using today's date.
### Step 9: Report back
After generating the file, report:
- File path
- Total notes found, original vs. restacks
- Date range actually covered
- Top 3 notes by total engagement (date + preview + stats)
- Any notes or warnings (e.g., page didn't fully load, some dates were ambiguous)
---
## Quality Checks
- [ ] All three required inputs were confirmed before starting
- [ ] Rate limiting honored: 2-second delay between paginated requests
- [ ] Author filter applied correctly — restacks are included as rows but flagged, not silently dropped
- [ ] Date range filter applied — no notes outside the window appear in the data
- [ ] Total Engagement column is Likes + Comments + Restacks (not hardcoded)
- [ ] Top 20% highlight is based on the actual data distribution, not a fixed threshold
- [ ] Header row is frozen and auto-filter is active
- [ ] Summary sheet stats reference only `original` notes, not restacks
- [ ] File is named with the author handle and today's date
- [ ] If the page failed to load properly, the user was told — not silently given an empty file
---
## Example Trigger Phrases
- "Scrape my Substack Notes and export to Excel — my handle is @handle, last 60 days"
- "Use the substack-notes-scraper skill on https://substack.com/@handle/notes for Q1 2026"
- "Pull my notes engagement data into a spreadsheet"
- "Export my Substack Notes stats with likes and restacks — author: Jane Smith, JanMar 2026"
- "Run the Substack scraper on my notes page and show me which posts performed best"
@@ -0,0 +1,625 @@
---
name: thumbnail-creator
description: "Generate article or newsletter thumbnail candidates using the Gemini API from inside Claude Code. Claude reads article copy, proposes composition concepts, writes image generation prompts incorporating brand specs, calls Gemini to generate the images, evaluates the results via computer vision, and returns ranked candidates with rationale. Use when asked to create thumbnails, generate cover images, or produce visual candidates for an article or newsletter."
---
# Thumbnail Creator Skill (via Gemini)
Generates article and newsletter thumbnail candidates by acting as an image-generation agent inside Claude Code. Instead of switching between tools and prompting Gemini's web UI one image at a time, this skill makes Claude do the full loop: read the copy, propose compositions, write tailored prompts, call the Gemini API, evaluate the outputs, and return ranked results with brief rationale.
The output is production-ready thumbnail candidates you can drop directly into your CMS, newsletter tool, or social scheduler.
---
## Prerequisites
Both of these must be in place before the skill can generate images:
### 1. Gemini API Key
Get a free key from [Google AI Studio](https://aistudio.google.com/app/apikey).
Set it as an environment variable:
```bash
export GEMINI_API_KEY="your-key-here"
```
To persist it across sessions, add to your shell profile (`~/.zshrc` or `~/.bashrc`):
```bash
echo 'export GEMINI_API_KEY="your-key-here"' >> ~/.zshrc
source ~/.zshrc
```
Verify it is set:
```bash
echo $GEMINI_API_KEY
```
### 2. generate_image.py Script
This script must exist at `./generate_image.py` in the project root. The full template is provided in the Script Template section below. Claude will check for it and offer to create it if missing.
**Python dependencies:**
```bash
pip install google-generativeai Pillow requests
```
Or with uv:
```bash
uv pip install google-generativeai Pillow requests
```
---
## Required Inputs
Claude will ask for these if not provided:
| Input | Required | Notes |
|---|---|---|
| Article copy or URL | Yes | Paste the full article text, or provide a URL to fetch. Used to extract themes, hooks, and key claims for composition. |
| Brand colours | Recommended | Hex codes or descriptive names. E.g. `#1A1A2E` (navy), `#E94560` (coral). If not provided, Claude uses clean neutral defaults. |
| Fonts / type style | Recommended | E.g. "bold sans-serif", "editorial serif", "Neue Haas Grotesk". Used in prompt to guide text treatment. |
| Style reference description | Recommended | E.g. "flat illustration, minimal, like Stripe's marketing site" or "photorealistic, dark background, high contrast". A style image URL can also be provided. |
| Output dimensions | No | Defaults to `1792x1024` (landscape, standard article thumbnail). Options: `1024x1024` (square), `1024x1792` (portrait/mobile). |
| Number of candidates | No | Defaults to 4. Min 1, max 8 (API limits and cost). |
| Article title (if different from H1) | No | Used as the primary text element in image prompts. |
| Candidate selection | No | After proposing compositions, Claude asks which to generate. User can say "all" or pick by number. |
---
## Output Structure
### Phase 1 — Composition Proposals (text, before any API calls)
Claude presents 3-4 composition concepts for user approval. Format:
```
Composition Concepts for: "[Article Title]"
1. BOLD CLAIM
Layout: Full-bleed dark background, large white headline centred,
single accent data point (e.g. "3x faster") in brand colour below
Mood: High authority, newsletter-style
Best for: LinkedIn, Substack headers
Rationale: The article's central claim ("X outperforms Y by 3x") is specific
enough to anchor the visual — readers stop on data.
2. CONCEPTUAL OBJECT
Layout: Central object illustration (e.g. a broken clock for a time-waste article),
title in upper third, minimal texture background
Mood: Editorial, Medium-style
Best for: Blog header, Medium cover, email preheader
Rationale: Gives art directors visual metaphor flexibility; works across sizes.
3. CONTRAST SPLIT
Layout: Left half brand colour, right half white or image,
title on colour side, supporting subtext on white side
Mood: Clean, professional, startup-brand feel
Best for: Newsletter, LinkedIn carousel first slide
Rationale: Split layout performs consistently in newsletter A/B tests;
text is readable at small sizes.
4. TYPOGRAPHIC ONLY
Layout: No illustration, oversized title treatment,
author name in small caps at bottom, thin rule separator
Mood: Premium, confident, editorial
Best for: Substack, Ghost, high-density email lists
Rationale: Works when the brand has strong type identity. Fastest to produce.
Which compositions do you want generated? (Reply with numbers, e.g. "1, 3" or "all")
```
### Phase 2 — Generated Image Files
After generation, Claude saves files to `./thumbnails/[article-slug]/`:
```
thumbnails/
└── article-slug-from-title/
├── candidate_01_bold_claim.png
├── candidate_02_conceptual_object.png
├── candidate_03_contrast_split.png
├── candidate_04_typographic.png
└── evaluation_report.md
```
### Phase 3 — Evaluation Summary Table
Claude evaluates each returned image via computer vision and produces:
```
Thumbnail Evaluation — "[Article Title]"
Generated: 2026-05-27 | Model: Gemini Imagen | Dimensions: 1792x1024
| # | Candidate | Composition | Brand Fit /10 | Text Legibility /10 | Recommendation |
|---|---|---|---|---|---|
| 1 | candidate_01_bold_claim.png | Bold Claim | 9 | 8 | ★ Top pick — strong data anchor, brand colours correct, title readable at 200px width |
| 2 | candidate_02_conceptual_object.png | Conceptual Object | 7 | 9 | Good fallback — legible, clean, but illustration style drifted slightly from brand |
| 3 | candidate_03_contrast_split.png | Contrast Split | 8 | 7 | Works well at full size; test at thumbnail size before publishing — right side text tightens |
| 4 | candidate_04_typographic.png | Typographic | 9 | 10 | Strongest for email — zero brand drift risk, completely text-based |
Recommended for web: candidate_01_bold_claim.png
Recommended for email/mobile: candidate_04_typographic.png
Recommended for social: candidate_03_contrast_split.png
Files saved to: ./thumbnails/article-slug-from-title/
```
---
## How Claude Should Execute This Skill
### Step 1 — Ingest and analyse the article
Accept article copy as pasted text or a URL.
If a URL is provided, fetch the page and extract:
- The H1 title
- The first 3-5 paragraphs (the hook, central claim, and key points)
- Any notable statistics or named frameworks mentioned
- The author name (for typographic compositions)
If text is pasted, read it directly. Focus on:
- **The hook:** What is the opening claim or tension?
- **The central thesis:** What is the one thing the article argues or teaches?
- **Key specifics:** Any numbers, named frameworks, or concrete examples that could anchor a visual
- **Tone:** Is this formal/authoritative, conversational/accessible, provocative/challenge-based?
Summarise these findings internally before proposing compositions — the proposals should feel tailored to this specific article, not generic.
### Step 2 — Collect brand specs
Ask the user for brand specs if not provided:
```
To generate on-brand thumbnails, I need a few details:
1. Brand colours (hex codes or descriptions) — e.g. #1A1A2E, #E94560
2. Font style preference — e.g. "bold sans-serif", "editorial serif", "geometric"
3. Visual style — e.g. "flat minimal", "photorealistic", "illustrated", "typographic only"
4. Any style references — describe a brand or publication whose aesthetic you want to match,
or share an image URL
If you don't have brand specs yet, say "use clean defaults" and I'll use a professional
dark-on-white editorial style.
```
If the user says "use clean defaults", apply:
- Background: `#FFFFFF` or `#0F0F0F` (dark mode default)
- Accent: `#2563EB` (blue)
- Font style: bold geometric sans-serif
- Style: minimal flat, no textures, high contrast
### Step 3 — Propose composition concepts
Write 3-4 composition concepts tailored to the article's tone and content. Each concept must:
- Have a name (short, memorable label)
- Describe the layout precisely (where title goes, what visual element anchors it, background treatment)
- Note the mood and the use case it's best suited for
- Include a rationale sentence explaining why this composition fits this specific article
After presenting the concepts, ask which to generate. Wait for user confirmation before making any API calls.
### Step 4 — Write Gemini image generation prompts
For each selected composition, write a detailed image generation prompt. Image generation prompts follow a different grammar than text prompts — they are descriptive, not instructional.
**Prompt structure:**
```
[Subject/composition] + [Style] + [Colour palette] + [Mood/lighting] +
[Text treatment if any] + [What to avoid]
```
**Example prompt for Bold Claim composition:**
```
Article thumbnail image. Large bold white sans-serif headline text reading "3x Faster Than
Traditional Methods" centred on a deep navy blue background (#1A1A2E). Small coral accent
text (#E94560) below reading the subtitle. Minimal flat design, no gradients, no stock photo
elements, no people. Clean professional editorial style, high contrast, newsletter header
format, 16:9 landscape orientation. The composition is typographic — text is the hero,
no illustration required. Avoid: clip art, drop shadows, low contrast, crowded layout.
```
**Prompt rules:**
- Include exact hex colours when brand colours are provided
- Specify the exact headline text to appear in the image
- Name the style explicitly ("flat design", "editorial", "photorealistic") — Gemini responds well to style category names
- Add a negative prompt ("Avoid: ...") at the end to reduce drift from brand style
- Keep prompts under 300 words — longer prompts do not reliably produce better outputs
### Step 5 — Check prerequisites and run the generation script
Before calling the API, verify:
```bash
# Check API key is set
echo $GEMINI_API_KEY
# Check script exists
ls -la ./generate_image.py
# Check dependencies
python3 -c "import google.generativeai, PIL, requests; print('Dependencies OK')"
```
If the script is missing, offer to create it using the template in the Script Template section below.
Run the generation script for each prompt:
```bash
python3 generate_image.py \
--prompt "your full prompt here" \
--output "./thumbnails/article-slug/candidate_01_bold_claim.png" \
--width 1792 \
--height 1024
```
Or pass all prompts in a batch config file:
```bash
python3 generate_image.py --config ./thumbnails/article-slug/prompts.json
```
### Step 6 — Evaluate generated images
After each image is saved, examine it using computer vision. Evaluate on two dimensions:
**Brand Fit (score /10):**
- Are the brand colours correct? (1-2 points each)
- Does the style match the requested aesthetic? (2 points)
- Is the layout consistent with the composition brief? (2 points)
- Are there any AI artefacts, distorted text, or unintended elements? (-1 per issue)
**Text Legibility (score /10):**
- Is the headline text readable at full resolution? (3 points)
- Is the headline text readable when the image is scaled to 300px wide (thumbnail size)? (3 points)
- Is there sufficient contrast between text and background? (2 points)
- Is the text placement within safe zones (not cut off at edges)? (2 points)
Note: Gemini Imagen sometimes renders text with spelling errors or distorted letterforms. If this happens, note it in the evaluation and suggest the user add the text overlay manually in Canva or Figma.
### Step 7 — Produce the evaluation report
Write the evaluation summary table (format shown in Output Structure section) and save it as `evaluation_report.md` in the output folder.
Include:
- One-line rationale for each score
- A top pick recommendation per use case (web, email/mobile, social)
- Any production notes (e.g. "text rendering is imperfect on candidate_02 — overlay text manually")
- The full prompts used, so the user can iterate directly in AI Studio if needed
### Step 8 — Offer iteration
After delivering the candidates, offer one iteration pass:
```
Want me to iterate on any of these?
Options:
- Adjust colours or style on a specific candidate
- Try a different composition concept
- Change the headline text
- Rerun with different Gemini parameters (different temperature/seed)
- Generate additional variants of the top pick
Just tell me what to change.
```
---
## Script Template
Claude should offer to write this file if `generate_image.py` is not present. This is the canonical template to use.
```python
#!/usr/bin/env python3
"""
generate_image.py — Gemini Imagen wrapper for Thumbnail Creator skill.
Usage:
python3 generate_image.py --prompt "..." --output "./out.png" [--width 1792] [--height 1024]
python3 generate_image.py --config ./prompts.json
Config JSON format:
[
{
"prompt": "...",
"output": "./thumbnails/slug/candidate_01.png",
"width": 1792,
"height": 1024
}
]
Requirements:
pip install google-generativeai Pillow
"""
import os
import sys
import json
import argparse
import base64
from pathlib import Path
try:
import google.generativeai as genai
from google.generativeai import types as genai_types
except ImportError:
print("ERROR: google-generativeai not installed. Run: pip install google-generativeai")
sys.exit(1)
try:
from PIL import Image
import io
except ImportError:
print("ERROR: Pillow not installed. Run: pip install Pillow")
sys.exit(1)
def get_api_key() -> str:
key = os.environ.get("GEMINI_API_KEY", "")
if not key:
print("ERROR: GEMINI_API_KEY environment variable is not set.")
print("Get a key at: https://aistudio.google.com/app/apikey")
print("Then run: export GEMINI_API_KEY='your-key-here'")
sys.exit(1)
return key
def generate_image(
prompt: str,
output_path: str,
width: int = 1792,
height: int = 1024,
) -> bool:
"""
Call Gemini Imagen to generate a single image and save it to output_path.
Returns True on success, False on failure.
"""
api_key = get_api_key()
genai.configure(api_key=api_key)
# Determine aspect ratio from dimensions
ratio = width / height
if abs(ratio - 16/9) < 0.1:
aspect_ratio = "16:9"
elif abs(ratio - 1.0) < 0.1:
aspect_ratio = "1:1"
elif abs(ratio - 9/16) < 0.1:
aspect_ratio = "9:16"
else:
aspect_ratio = "16:9" # default fallback
try:
imagen_model = genai.ImageGenerationModel("imagen-3.0-generate-002")
result = imagen_model.generate_images(
prompt=prompt,
number_of_images=1,
aspect_ratio=aspect_ratio,
safety_filter_level="block_only_high",
person_generation="allow_adult",
)
if not result.images:
print(f" No images returned for: {output_path}")
return False
image_data = result.images[0]
# Ensure output directory exists
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
# Save the image
if hasattr(image_data, '_image_bytes'):
img_bytes = image_data._image_bytes
elif hasattr(image_data, 'image'):
img_bytes = image_data.image
else:
# Fallback: try to access raw data
img_bytes = bytes(image_data)
img = Image.open(io.BytesIO(img_bytes))
# Resize to exact dimensions if needed
if img.size != (width, height):
img = img.resize((width, height), Image.LANCZOS)
img.save(output_path, format="PNG", optimize=True)
print(f" Saved: {output_path} ({img.size[0]}x{img.size[1]})")
return True
except Exception as e:
print(f" ERROR generating image: {e}")
return False
def run_from_args():
parser = argparse.ArgumentParser(description="Gemini Imagen wrapper for thumbnail generation")
parser.add_argument("--prompt", type=str, help="Image generation prompt")
parser.add_argument("--output", type=str, help="Output file path (.png)")
parser.add_argument("--width", type=int, default=1792, help="Image width in pixels")
parser.add_argument("--height", type=int, default=1024, help="Image height in pixels")
parser.add_argument("--config", type=str, help="JSON config file with batch of prompts")
args = parser.parse_args()
if args.config:
# Batch mode
with open(args.config, "r") as f:
items = json.load(f)
print(f"Batch mode: {len(items)} image(s) to generate")
results = []
for i, item in enumerate(items, start=1):
print(f"\n[{i}/{len(items)}] Generating: {item['output']}")
ok = generate_image(
prompt=item["prompt"],
output_path=item["output"],
width=item.get("width", 1792),
height=item.get("height", 1024),
)
results.append({"output": item["output"], "ok": ok})
print(f"\nBatch complete: {sum(r['ok'] for r in results)}/{len(results)} succeeded")
for r in results:
status = "OK " if r["ok"] else "ERR"
print(f" {status} {r['output']}")
elif args.prompt and args.output:
# Single image mode
print(f"Generating: {args.output}")
ok = generate_image(
prompt=args.prompt,
output_path=args.output,
width=args.width,
height=args.height,
)
if ok:
print("Done.")
else:
print("Failed.")
sys.exit(1)
else:
parser.print_help()
sys.exit(1)
if __name__ == "__main__":
run_from_args()
```
**To create this file from inside Claude Code:**
```bash
# Claude will write this file if it doesn't exist:
ls ./generate_image.py || echo "Script missing — Claude will create it"
```
---
## Prompt Writing Reference
Claude should use this reference when writing image generation prompts. These patterns produce the most consistent results with Gemini Imagen.
### Composition patterns
| Composition type | Prompt anchor phrase |
|---|---|
| Text-led, dark background | "Bold white sans-serif headline text on deep [colour] background, minimal flat design" |
| Text-led, light background | "High-contrast black headline text on clean white background, editorial layout" |
| Object/illustration centred | "Centred [object] illustration, [style], [colour] background, title text in upper third" |
| Split layout | "Vertical split: left half [colour], right half white. Headline on left side, supporting text on right" |
| Photography style | "Photorealistic [scene description], [mood] lighting, [colour] colour grade, text overlay area at [position]" |
### Style modifiers that work well with Gemini
- `flat design, no gradients` — clean vector-style outputs
- `editorial magazine style` — sophisticated, typographic
- `minimal, lots of whitespace` — reduces visual noise
- `high contrast, bold typography` — strong thumbnail legibility
- `Bauhaus-inspired` — geometric, structured
- `dark mode aesthetic` — dark backgrounds with light text
- `startup marketing style` — clean, optimistic, sans-serif
### Negative prompts (always include)
Append to every prompt:
```
Avoid: stock photography clichés, clipart, excessive gradients, drop shadows,
cluttered layout, lens flares, watermarks, low contrast text, AI artefacts.
```
### Text rendering note
Gemini Imagen sometimes renders short text phrases accurately and longer headlines poorly. If the article headline is longer than 6 words, consider splitting it in the prompt:
```
Primary headline: "[First 4-5 words]"
Secondary text: "[Remaining words]"
```
Or instruct the user to add text overlay manually in Canva after generation if legibility is critical.
---
## Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| `GEMINI_API_KEY not set` | Environment variable missing | Run `export GEMINI_API_KEY="your-key"` and retry |
| `ModuleNotFoundError: google.generativeai` | Dependency missing | Run `pip install google-generativeai` |
| `No images returned` | Safety filter triggered | Revise prompt to remove any ambiguous language; check that the prompt doesn't describe faces, violence, or brand logos |
| Generated image has garbled text | Imagen text rendering limitation | Use shorter headline in prompt, or plan to add text overlay in Canva/Figma post-generation |
| Image is the wrong size | Aspect ratio mismatch | Confirm `--width` and `--height` args match one of the supported ratios (16:9, 1:1, 9:16) |
| `generate_image.py not found` | Script not created yet | Ask Claude to create it using the template above |
| API quota exceeded | Free tier limit | Wait or upgrade to Gemini API paid tier |
| Style drift from brand | Prompt not specific enough | Add exact hex codes and specific style descriptors; add stronger negative prompt |
---
## Quality Checks
Before marking the task complete, verify each item:
- [ ] `GEMINI_API_KEY` environment variable confirmed set before any API calls
- [ ] `generate_image.py` script exists in project root — created from template if missing
- [ ] All Python dependencies installed and verified (`google-generativeai`, `Pillow`)
- [ ] Composition proposals were presented and user confirmed which to generate before any API calls
- [ ] Each composition proposal is specific to this article's content — not generic placeholders
- [ ] Brand colours (hex codes) are included in the image generation prompts
- [ ] Negative prompt appended to every image generation prompt
- [ ] Headline text in prompts is 6 words or fewer per text element (longer headlines split or noted as overlay candidates)
- [ ] Output folder created at `./thumbnails/[article-slug]/` with correct slug derived from article title
- [ ] Files named with candidate number and composition name (`candidate_01_bold_claim.png`)
- [ ] Each generated image evaluated via computer vision — not assumed to be correct
- [ ] Brand Fit and Text Legibility scores are specific and justified, not round numbers
- [ ] Any text rendering issues noted in evaluation with "add text overlay manually" recommendation
- [ ] Evaluation report saved as `evaluation_report.md` in the output folder
- [ ] At least one recommendation given per use case: web, email/mobile, social
- [ ] Full prompts used are included in the evaluation report for user iteration reference
- [ ] Iteration offer made after delivering results
---
## Example Trigger Phrases
- "Create thumbnails for this article"
- "Generate cover image candidates for my newsletter"
- "Make me 4 thumbnail options for this post"
- "Can you generate some thumbnail ideas using Gemini?"
- "I need a featured image for this article — use my brand colours"
- "Create a thumbnail for this piece using Gemini" [followed by article text or URL]
- "Generate article cover images for these brand specs: [colours, style]"
- "Make thumbnail candidates and rank them"
- "I need newsletter header images — here's the copy"
- "Generate and evaluate thumbnail options for this draft"
- "Use Gemini to create cover image options"
- "Thumbnail this article" [followed by article text]
- "Create 3 thumbnail compositions and pick the best one"
---
## Cost and Rate Limits
**Gemini AI Studio free tier (as of early 2026):**
- Imagen 3: 10 images per day (free)
- Rate limit: varies by region and account tier
**Paid tier:**
- Imagen 3 pricing: approximately $0.03-0.05 per image (check current Google Cloud pricing)
- For a typical session generating 4-8 candidates, total cost is under $0.40
**Recommendation:**
- Use the free tier for exploration and iteration
- Generate final production candidates on paid tier for higher daily limits
- For newsletter teams generating thumbnails weekly, the paid tier is more practical
---
*Originally created by Karen Spinner (Wondering About AI) — adapted and extended for this library.*