
Carousel Generator
- 36 installs
- 5 repo stars
- Updated June 18, 2026
- drshailesh88/integrated_content_os
Generate branded 1080x1080px Instagram carousel slides from text or markdown input with brand colors, typography, and footer.
About
This skill generates branded 1080x1080px Instagram carousel slides from text or markdown, applying set brand colors, typography, and a consistent footer. Content creators use it to produce ready-to-post carousels for specific Instagram accounts.
- Automatic text wrapping, bullet rendering, and title/content slide styles
- Supports multiple brand accounts with configurable max slides and output dir
Carousel Generator by the numbers
- 36 all-time installs (skills.sh)
- Ranked #944 of 1,337 Generative Media skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/drshailesh88/integrated_content_os --skill carousel-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 36 |
|---|---|
| repo stars | ★ 5 |
| Last updated | June 18, 2026 |
| Repository | drshailesh88/integrated_content_os ↗ |
What it does
Generate branded 1080x1080px Instagram carousel slides from text or markdown input with brand colors, typography, and footer.
Files
Carousel Generator
Generate branded Instagram carousels (1080x1080px) from text content.
Quick Start
cd "/Users/shaileshsingh/integrated cowriting system/skills/cardiology/carousel-generator"
python tools/generate-carousel.py <input_file> <account> [options]
# Examples:
python tools/generate-carousel.py content.txt 1 # Account 1: @heartdocshailesh
python tools/generate-carousel.py content.txt 2 # Account 2: @dr.shailesh.singh
python tools/generate-carousel.py content.txt 1 -m 8 # Max 8 slides
python tools/generate-carousel.py content.txt 1 -o ~/out # Custom output dirFeatures
- 1080x1080px Instagram carousel slides
- Brand colors (Deep Teal, Mist Aqua, Warm Coral)
- Consistent footer with name and handle
- Automatic text wrapping
- Bullet point rendering
- Title slides (teal background) + content slides (light background)
Input Formats
1. Plain Text with Markdown
# Main Title
Subtitle or intro
---
## Slide 2 Title
• Bullet point 1
• Bullet point 2
---
## Slide 3
Body text with automatic wrapping.2. JSON Format
[
{"title": "Title Slide", "body": "Subtitle", "type": "title"},
{"title": "Content Slide", "body": "Body text", "type": "content"}
]Output
PNG files in output/carousels/<name>/account-<n>/
Brand Specifications
Colors
COLORS = {
'primary': '#207178', # Deep Teal - titles, CTAs
'secondary': '#E4F1EF', # Mist Aqua - backgrounds
'accent': '#F28C81', # Warm Coral - bullets, highlights
'neutral_light': '#F8F9FA', # Off-White - alt backgrounds
'neutral_dark': '#333333', # Charcoal - body text
'alert': '#E63946' # Heart Red - emphasis
}Accounts
- Account 1:
@heartdocshailesh - Account 2:
@dr.shailesh.singh
Typography
- Font: Inter (Bold, SemiBold, Regular, Medium)
- Fallback: Helvetica (macOS) -> System default
Dependencies
- Python 3.11+
- Pillow (PIL) for image generation
- No API keys required (runs locally)
pip install pillowNote
This skill is called by the content-os orchestrator for carousel generation. You can also use it standalone for quick carousel creation.
ICP (Ideal Customer Profile) - Audience Archetypes
Overview
6 distinct audience segments, each with different awareness levels, pain points, and content needs.
---
Archetype 1: Heart Patients (Post-Event)
Demographics
- Age: 45-70
- Just had: Heart attack, stent, bypass, or diagnosis
- Location: Urban/Semi-urban India
- Education: Varied
Psychographics
- Fear: "Will this happen again?"
- Confusion: Conflicting advice from family, internet, other doctors
- Hope: Wanting to believe lifestyle changes work
- Trust issues: Skeptical after medical system experiences
Awareness Level
Solution Aware → Know they need to change, seeking HOW
Content Needs
- Clear medication guidance
- Lifestyle do's and don'ts
- Recovery timelines
- When to worry vs. when to relax
- Success stories of recovery
Trigger Phrases
- "Stent ke baad kya khana chahiye"
- "Heart attack dobara na ho"
- "Bypass surgery recovery"
---
Archetype 2: Lifestyle Disease Group
Demographics
- Age: 35-60
- Conditions: Diabetes, Hypertension, Metabolic Syndrome, Obesity
- Often: Multiple conditions overlapping
Psychographics
- Denial: "It's not that serious yet"
- Overwhelm: Too many things to change
- Medication resistance: "Goli nahi leni, natural chahiye"
- Guilt: Know they should do better
Awareness Level
Problem Aware → Know something's wrong, not sure how serious
Content Needs
- Wake-up calls with data (not fear-mongering)
- Incremental change strategies
- Medication myth-busting
- Connection between conditions (diabetes → heart)
- Practical diet/exercise for Indian lifestyle
Trigger Phrases
- "Sugar control kaise karein"
- "BP ki goli lifetime leni padegi?"
- "Diabetes se heart attack ka connection"
---
Archetype 3: Proactive Biohackers
Demographics
- Age: 30-50
- Profile: High-income, educated professionals
- Interest: Longevity, optimization, prevention
Psychographics
- Data-driven: Want numbers, biomarkers
- Proactive: Prevention over cure
- Skeptical: Question mainstream advice
- Investment mindset: Health as ROI
Awareness Level
Product Aware → Know specific tests/interventions, comparing options
Content Needs
- Advanced biomarkers (ApoB, Lp(a), CAC score)
- Latest research and studies
- Test interpretation
- Supplement evidence review
- Longevity protocols
Trigger Phrases
- "ApoB vs LDL"
- "Lp(a) kya hai"
- "CAC score interpretation"
- "Zone 2 cardio for heart health"
---
Archetype 4: Health Enthusiasts
Demographics
- Age: 25-45
- Profile: Fitness-conscious, some medical knowledge
- Motivation: General wellness, family health
Psychographics
- Curious: Wants to understand why
- Myth-skeptic: Questions viral health claims
- Sharers: Will forward good content to family
- Moderate: Balanced approach, not extreme
Awareness Level
Solution Aware → Knows lifestyle matters, wants evidence-based guidance
Content Needs
- Myth-busting content
- Science explanations made simple
- Practical tips that fit Indian lifestyle
- Family health management
- Exercise and diet evidence
Trigger Phrases
- "Desi ghee heart ke liye theek hai?"
- "Walking vs gym"
- "Best diet for Indians"
---
Archetype 5: Caregivers
Demographics
- Age: 30-55
- Role: Spouse, adult children of heart patients
- Gender: Often women managing family health
Psychographics
- Anxiety: Constant worry about loved one
- Responsibility: Feel they must manage everything
- Information hunger: Want to be prepared
- Guilt: When something goes wrong
Awareness Level
Problem Aware → Know the risk, need actionable guidance
Content Needs
- Warning signs to watch for
- Emergency protocols
- How to support without nagging
- Cooking/diet for heart patients
- Genetic/family risk assessment
Trigger Phrases
- "Heart attack ke symptoms"
- "Papa ko kya khilayein"
- "Family history means kya"
---
Archetype 6: Young & Anxious
Demographics
- Age: 25-40
- Profile: Young professionals, often after health scare or family event
- Trigger: Parent's heart attack, routine test abnormality, viral content
Psychographics
- Health anxiety: Hypochondriac tendencies
- Information overload: Dr. Google syndrome
- Reassurance-seeking: Want to hear "you're fine"
- Prevention-minded: Don't want parents' fate
Awareness Level
Unaware to Problem Aware → May not have real problem, but worried
Content Needs
- When to worry vs. when not to
- Normal vs. abnormal explained
- Early screening recommendations
- Anxiety vs. real symptoms
- Lifestyle for prevention (not treatment)
Trigger Phrases
- "Chest pain at 30"
- "High cholesterol at young age"
- "Should I get tested"
- "Panic attack vs heart attack"
---
Awareness Level Framework
| Level | State | Content Strategy |
|---|---|---|
| Unaware | Don't know they have a problem | Educational, surprising facts |
| Problem Aware | Know problem, not solutions | Explain consequences + hope |
| Solution Aware | Know solutions exist, comparing | Evidence + practical guidance |
| Product Aware | Know specific interventions | Detailed how-to + nuance |
| Most Aware | Ready to act | Direct CTA, remove friction |
---
Content-Archetype Matrix
| Content Type | Best For Archetypes |
|---|---|
| Myth-busting | 2, 4, 6 |
| Data/Research deep-dives | 3, 4 |
| Recovery guides | 1, 5 |
| Emergency/Warning signs | 1, 5, 6 |
| Lifestyle tips | 2, 4 |
| Advanced biomarkers | 3 |
| Reassurance content | 6 |
| Family screening | 3, 5 |
---
Engagement Patterns by Archetype
| Archetype | Watch Time | Comments | Shares |
|---|---|---|---|
| Heart Patients | Long (desperate for info) | Questions | To family |
| Lifestyle Disease | Medium | Skeptical | Low |
| Biohackers | Full (if data-rich) | Technical Qs | To similar people |
| Health Enthusiasts | Medium-Long | Supportive | High (family) |
| Caregivers | Full | Practical Qs | To patient |
| Young & Anxious | Short-Medium | Reassurance Qs | Low |
---
Use this profile to tailor hooks, depth, tone, and CTAs for each piece of content.
Content Frameworks
Ship 30 for 30 Framework
4A Categories
Every piece of content should fit one of these categories:
1. Actionable - Practical tips they can use today 2. Analytical - Data-driven insights and research 3. Aspirational - Motivational, showing what's possible 4. Anthropological - Stories and human experiences
10 Magical Ways (Content Modifiers)
View any topic through these 10 lenses:
1. Tips - "5 Tips for..." (Actionable advice) 2. Stats - "The Data Shows..." (Numbers and research) 3. Steps - "How to... in 5 Steps" (Process breakdown) 4. Lessons - "What I Learned..." (Experience insights) 5. Examples - "Case Study:..." (Real scenarios) 6. Reasons - "Why X Matters" (The logic behind) 7. Mistakes - "Avoid These Errors" (Common pitfalls) 8. Questions - "Have You Considered?" (Thought-provoking) 9. Stories - "The Patient Who..." (Narrative) 10. Benefits - "The Results of..." (Outcomes)
Headline Formula
[Number] + [What] + [For Whom] + [Outcome] + [Curiosity]
Examples:
- "5 Heart Attack Signs Every Diabetic Should Know"
- "3 Foods That Actually Lower Cholesterol (Research Says)"
- "Why Statins Failed This Patient (And What Worked)"
Skimmability Rhythms
- 1/3/1 - One point, three supporting bullets, one conclusion
- 1/5/1 - Hook, five main points, CTA
- 3/3/3 - Three sections, three points each, three words per key phrase
---
Cole GPT Method (Mini-Model Technique)
Core Principle
User provides first draft → AI generates 5 variations → User picks best elements → Iterate
90% Value Rule
90% of content value comes from:
- Headline (hook)
- 3 Main Points (structure)
Workflow
1. User writes rough draft or bullet points 2. AI generates 5 headline variations 3. User picks best headline 4. AI generates 5 body variations 5. User picks and combines best elements 6. Final polish
---
Anti-AI Detection Guidelines
AVOID (AI Tells)
- "It's important to note that..."
- "In conclusion" / "In summary"
- "Stands as a testament to..."
- "Plays a vital role in..."
- Excessive em dashes (—) - MAX 1 per paragraph
- Rule of three lists everywhere
- "Groundbreaking", "game-changing", "revolutionary"
- "Nestled", "breathtaking", "vibrant tapestry"
- Starting sentences with "So," or "Well,"
USE (Human Patterns)
- Specific data with actual numbers
- Proper source attribution (journal names)
- Natural, varied sentence lengths
- First-person clinical insights ("In my clinic, I see...")
- Statistics: HR, NNT, CI when available
- Occasional incomplete sentences. Like this.
- Questions to the reader
- Contractions (don't, won't, can't)
- Colloquial transitions ("Look," "Here's the thing," "Thing is,")
Sentence Variation
- Mix short. Medium length sentences work too. And then throw in a longer sentence that develops the idea more fully before concluding with another short one.
- Never more than 2 sentences in a row with same structure
- Vary paragraph lengths (1-5 sentences)
---
YouTube Structure (Hinglish)
Think School Approach
1. Thesis Hook - State the controversial/surprising claim upfront 2. Data as Plot Points - Each data point moves the story forward 3. Logical Flow - Clear cause → effect → implication 4. Strong Conclusion - Tie back to thesis with actionable insight
Neelesh Misra Pacing
1. Empathetic Start - Connect emotionally first 2. Simple Hindustani - Hindi narrative, English for technical terms 3. Strategic Pauses - Let important points sink in 4. Warm Tone - Like talking to a friend
Language Balance
- 70% Hindi (narrative, emotion, flow)
- 30% English (medical terms, data, precision)
Opening Formula
"Dekhiye..." followed by:
- A surprising statistic
- A patient story
- A common misconception
---
Thread Structure (Twitter)
Hook Tweet
- Strong statement or question
- Numbers or specific claim
- Create curiosity gap
Body Tweets (3-7)
- One idea per tweet
- Build on previous tweet
- Use line breaks for readability
Closing Tweet
- Summarize key insight
- Clear CTA (Follow, Retweet, Comment)
- Link to resource if applicable
Thread Types
1. Story Thread - Personal/patient narrative with lessons 2. Framework Thread - Step-by-step process or system 3. Myth-Busting Thread - Common beliefs vs. evidence
---
Content Quality Checklist
Before publishing:
- [ ] Passes anti-AI detection read
- [ ] Fits one of 4A categories
- [ ] Has specific data/numbers
- [ ] Appropriate for target archetype
- [ ] Matches awareness level
- [ ] Strong hook/headline
- [ ] Clear CTA
- [ ] Natural sentence variation
- [ ] First-person voice where appropriate
Brand Specifications
Color Palette
| Name | Hex | RGB | Usage |
|---|---|---|---|
| Deep Teal | #207178 | (32, 113, 120) | Titles, CTAs, primary branding |
| Mist Aqua | #E4F1EF | (228, 241, 239) | Slide backgrounds, soft elements |
| Warm Coral | #F28C81 | (242, 140, 129) | Accent highlights, bullet points |
| Off-White | #F8F9FA | (248, 249, 250) | Alternative backgrounds |
| Charcoal | #333333 | (51, 51, 51) | Body text, high contrast |
| Heart Red | #E63946 | (230, 57, 70) | Emphasis, danger alerts |
Typography
Font Family
- Primary: Inter (all weights)
- Fallback 1: Helvetica (macOS system font)
- Fallback 2: Arial (Windows/Linux)
- Fallback 3: System default
Font Sizes
| Element | Size | Weight |
|---|---|---|
| Title (title slide) | 60px | Bold |
| Subtitle | 48px | SemiBold |
| Body text | 36px | Regular |
| Footer name | 28px | Medium |
| Footer handle | 24px | Regular |
Image Specifications
Instagram Carousel
- Dimensions: 1080 x 1080 px (square)
- Margin: 80px all sides
- Content area: 920 x 850 px
- Footer height: 150px
- Max slides: 10 (Instagram limit)
Instagram Infographic (Future)
- Dimensions: 1080 x 1350 px (portrait)
- Margin: 80px all sides
- Footer height: 150px
Footer Design
┌──────────────────────────────────────────┐
│ CONTENT │
│ │
├──────────────────────────────────────────┤
│ [Photo] Dr. Shailesh Singh │
│ @heartdocshailesh │
└──────────────────────────────────────────┘- Subtle separator line (Mist Aqua, 2px)
- Profile photo: 100x100px circular
- Photo positioned left, text to the right
- Name in Charcoal
- Handle in Deep Teal
Account Handles
| Account | Handle | Use Case |
|---|---|---|
| 1 | @heartdocshailesh | Primary cardiology content |
| 2 | @dr.shailesh.singh | Professional/clinical |
Title Slide Design
- Background: Deep Teal (#207178)
- Text: White (#FFFFFF)
- Centered vertically and horizontally
- Subtitle in Mist Aqua below title
Content Slide Design
- Background: Off-White (#F8F9FA)
- Title: Deep Teal, top-aligned
- Body: Charcoal
- Bullet points: Warm Coral circles (10px diameter)
- Footer at bottom
Accessibility
- Minimum contrast ratio: 4.5:1 for body text
- Title text on teal background: White (15.5:1 ratio)
- Body text on white: Charcoal (12.6:1 ratio)
- All text readable at mobile size
File Naming
- Slides:
slide-01.png,slide-02.png, etc. - Output folder:
output/carousels/<topic>/account-<n>/
Input Formats for Content-OS Generators
Overview
All content-os generators accept multiple input formats for flexibility. The generators auto-detect the format based on content structure.
Format 1: Markdown with Separators (Recommended)
Best for quick content creation. Use # for title slide, ## for content slides, --- for explicit slide breaks.
# Main Title
Optional subtitle text
---
## Slide 2 Title
Body text for this slide.
• Bullet point 1
• Bullet point 2
• Bullet point 3
---
## Slide 3 Title
- Also supports dash bullets
- Like this
---
## Final Slide
Call to action or summary here.Rules:
# Title= Title slide (teal background)## Title= Content slide title---= Explicit slide separator•or-or*= Bullet points- Blank lines are preserved as spacing
Format 2: JSON Array
Best for programmatic generation or complex layouts.
[
{
"title": "Main Title",
"body": "Subtitle text",
"type": "title"
},
{
"title": "Content Slide",
"body": "Body text here.\n• Bullet 1\n• Bullet 2",
"type": "content"
},
{
"title": "Another Slide",
"body": "More content",
"type": "content"
}
]Fields:
title(required): Slide title textbody(optional): Body content, use\nfor line breakstype(optional):"title"or"content", defaults to"content"
Format 3: JSON Object with Slides Array
Alternative JSON structure with metadata wrapper.
{
"slides": [
{"title": "Title", "body": "Subtitle", "type": "title"},
{"title": "Slide 2", "body": "Content", "type": "content"}
],
"metadata": {
"topic": "Hypertension",
"created": "2024-12-29"
}
}Format 4: Plain Text (Fallback)
If no structure is detected, content becomes a single slide.
This is just plain text.
It will all appear on one slide.Best Practices
For Educational Content
# 5 Signs of Heart Attack
Know these warning signs
---
## 1. Chest Discomfort
• Pressure or squeezing feeling
• Center of chest
• Lasts more than few minutes
---
## 2. Shortness of Breath
• May occur with or without chest pain
• Often the first symptom in women
---
## Take Action
Call emergency services immediately if you experience these symptoms.For Data-Heavy Content
# Statin Benefits
What the research shows
---
## JUPITER Trial Results
• 44% reduction in heart attacks
• 48% reduction in strokes
• NNT = 95 over 2 years
---
## Who Benefits Most?
• High-risk patients
• Those with elevated CRP
• Family history of early CVDFor Myth-Busting
# Cholesterol Myths Debunked
What you've been told vs. the truth
---
## Myth: Eggs are bad for your heart
TRUTH: For most people, dietary cholesterol has minimal impact on blood cholesterol. 1-3 eggs daily is fine for healthy adults.
---
## Myth: All fats are bad
TRUTH: Unsaturated fats (olive oil, nuts, fish) are heart-protective. It's trans fats you should avoid.Character Limits
| Element | Recommended | Maximum |
|---|---|---|
| Title | 40 chars | 80 chars |
| Body per slide | 200 chars | 400 chars |
| Bullet points | 3-5 per slide | 7 max |
| Total slides | 8 | 10 |
Tips
1. Keep it scannable: Use bullets over paragraphs 2. One idea per slide: Don't overcrowd 3. Strong title slide: Hook the viewer immediately 4. End with CTA: Follow, share, or learn more 5. Use data: Specific numbers are more compelling
# 5 Warning Signs of Heart Attack
Know these symptoms that could save your life
---
## 1. Chest Discomfort
• Pressure or squeezing feeling
• Center of chest
• Lasts more than a few minutes
• May come and go
---
## 2. Shortness of Breath
• With or without chest pain
• May occur before other symptoms
• Common first sign in women
---
## 3. Upper Body Pain
• Arms, back, neck, or jaw
• Often left arm
• Can feel like muscle strain
---
## 4. Cold Sweat & Nausea
• Breaking out in cold sweat
• Feeling sick to stomach
• Lightheadedness or dizziness
---
## 5. Don't Wait - Act FAST
• Time is muscle
• Call emergency services immediately
• Every minute counts for survival
#!/usr/bin/env python3
"""
Carousel Generator for Dr. Shailesh Singh
Generates 1080x1080px Instagram carousel slides from text content
Usage:
python generate-carousel.py <input_file> <account_number> [options]
account_number: 1 = @heartdocshailesh, 2 = @dr.shailesh.singh
Part of the Content-OS skill in the Integrated Cowriting System.
"""
import os
import sys
import argparse
import json
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from typing import List, Dict, Tuple
# Get skill directory for relative paths
SKILL_DIR = Path(__file__).parent.parent
KNOWLEDGE_BASE = SKILL_DIR / "knowledge-base"
OUTPUT_DIR = SKILL_DIR / "output"
# Brand color palette
COLORS = {
'primary': '#207178', # Deep Teal - titles, CTAs
'secondary': '#E4F1EF', # Mist Aqua - backgrounds
'accent': '#F28C81', # Warm Coral - icons, highlights
'neutral_light': '#F8F9FA', # Off-White - alt backgrounds
'neutral_dark': '#333333', # Charcoal - body text
'alert': '#E63946' # Heart Red - emphasis
}
# Image specifications
SLIDE_SIZE = (1080, 1080)
MARGIN = 80
FOOTER_HEIGHT = 150
CONTENT_WIDTH = SLIDE_SIZE[0] - (2 * MARGIN)
CONTENT_HEIGHT = SLIDE_SIZE[1] - FOOTER_HEIGHT - (2 * MARGIN)
# Account handles
ACCOUNTS = {
1: "@heartdocshailesh",
2: "@dr.shailesh.singh"
}
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
"""Convert hex color to RGB tuple"""
hex_color = hex_color.lstrip('#')
return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
def load_fonts():
"""Load Inter fonts or fallback to system fonts"""
fonts = {}
# Try to load Inter font (download if needed)
try:
fonts['title'] = ImageFont.truetype("Inter-Bold.ttf", 60)
fonts['subtitle'] = ImageFont.truetype("Inter-SemiBold.ttf", 48)
fonts['body'] = ImageFont.truetype("Inter-Regular.ttf", 36)
fonts['footer'] = ImageFont.truetype("Inter-Medium.ttf", 28)
fonts['footer_small'] = ImageFont.truetype("Inter-Regular.ttf", 24)
except OSError:
# Fallback to Helvetica/Arial
try:
fonts['title'] = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 60)
fonts['subtitle'] = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 48)
fonts['body'] = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 36)
fonts['footer'] = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 28)
fonts['footer_small'] = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 24)
except:
# Final fallback to default
fonts['title'] = ImageFont.load_default()
fonts['subtitle'] = ImageFont.load_default()
fonts['body'] = ImageFont.load_default()
fonts['footer'] = ImageFont.load_default()
fonts['footer_small'] = ImageFont.load_default()
return fonts
def wrap_text(text: str, font: ImageFont.FreeTypeFont, max_width: int, draw: ImageDraw.Draw) -> List[str]:
"""Wrap text to fit within max_width"""
words = text.split()
lines = []
current_line = []
for word in words:
test_line = ' '.join(current_line + [word])
bbox = draw.textbbox((0, 0), test_line, font=font)
width = bbox[2] - bbox[0]
if width <= max_width:
current_line.append(word)
else:
if current_line:
lines.append(' '.join(current_line))
current_line = [word]
if current_line:
lines.append(' '.join(current_line))
return lines
def parse_content(input_file: str) -> List[Dict[str, str]]:
"""
Parse input file into slide content
Supports plain text, JSON, or markdown format
"""
slides = []
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
# Try to parse as JSON first
try:
data = json.loads(content)
if isinstance(data, list):
return data
elif isinstance(data, dict) and 'slides' in data:
return data['slides']
except json.JSONDecodeError:
pass
# Parse as plain text with slide separators
# Format: lines starting with "##" are slide titles
# Following lines are body text until next "##" or "---"
lines = content.strip().split('\n')
current_slide = {'title': '', 'body': '', 'type': 'content'}
for line in lines:
line = line.strip()
if line.startswith('---'):
if current_slide['title'] or current_slide['body']:
slides.append(current_slide.copy())
current_slide = {'title': '', 'body': '', 'type': 'content'}
elif line.startswith('## '):
if current_slide['title'] or current_slide['body']:
slides.append(current_slide.copy())
current_slide = {'title': line[3:], 'body': '', 'type': 'content'}
elif line.startswith('# '):
if current_slide['title'] or current_slide['body']:
slides.append(current_slide.copy())
current_slide = {'title': line[2:], 'body': '', 'type': 'title'}
elif line:
if current_slide['body']:
current_slide['body'] += '\n' + line
else:
current_slide['body'] = line
if current_slide['title'] or current_slide['body']:
slides.append(current_slide)
# If no structured content found, create a single slide
if not slides and content.strip():
slides = [{'title': 'Content', 'body': content.strip(), 'type': 'content'}]
return slides
def draw_footer(draw: ImageDraw.Draw, fonts: Dict, account: int, photo_path: str = None):
"""Draw footer with profile photo and account handle"""
footer_y = SLIDE_SIZE[1] - FOOTER_HEIGHT + 20
# Draw subtle separator line
line_y = SLIDE_SIZE[1] - FOOTER_HEIGHT
draw.line([(MARGIN, line_y), (SLIDE_SIZE[0] - MARGIN, line_y)],
fill=hex_to_rgb(COLORS['secondary']), width=2)
# Try to load and paste profile photo
photo_x = MARGIN
text_x = MARGIN
if photo_path and os.path.exists(photo_path):
try:
photo = Image.open(photo_path)
photo = photo.resize((100, 100), Image.Resampling.LANCZOS)
# Create circular mask
mask = Image.new('L', (100, 100), 0)
mask_draw = ImageDraw.Draw(mask)
mask_draw.ellipse([(0, 0), (100, 100)], fill=255)
# Paste with circular mask
photo_pos = (photo_x, footer_y)
# Create a temporary image to paste onto
temp = Image.new('RGBA', (100, 100), (0, 0, 0, 0))
temp.paste(photo, (0, 0))
text_x = photo_x + 120
except Exception as e:
print(f"Warning: Could not load profile photo: {e}")
text_x = MARGIN
# Draw name and handle
name_text = "Dr. Shailesh Singh"
handle_text = ACCOUNTS[account]
name_y = footer_y + 20
handle_y = name_y + 35
draw.text((text_x, name_y), name_text,
fill=hex_to_rgb(COLORS['neutral_dark']), font=fonts['footer'])
draw.text((text_x, handle_y), handle_text,
fill=hex_to_rgb(COLORS['primary']), font=fonts['footer_small'])
def create_title_slide(slide_data: Dict, fonts: Dict, account: int, photo_path: str = None) -> Image.Image:
"""Create opening title slide"""
img = Image.new('RGB', SLIDE_SIZE, hex_to_rgb(COLORS['primary']))
draw = ImageDraw.Draw(img)
# Main title - centered, white text
title = slide_data.get('title', 'Title')
title_lines = wrap_text(title, fonts['title'], CONTENT_WIDTH, draw)
# Calculate vertical centering
line_height = 80
total_height = len(title_lines) * line_height
start_y = (SLIDE_SIZE[1] - total_height) // 2
for i, line in enumerate(title_lines):
bbox = draw.textbbox((0, 0), line, font=fonts['title'])
text_width = bbox[2] - bbox[0]
x = (SLIDE_SIZE[0] - text_width) // 2
y = start_y + (i * line_height)
draw.text((x, y), line, fill=(255, 255, 255), font=fonts['title'])
# Subtitle if provided
if slide_data.get('body'):
subtitle_y = start_y + total_height + 40
subtitle_lines = wrap_text(slide_data['body'], fonts['body'], CONTENT_WIDTH, draw)
for i, line in enumerate(subtitle_lines):
bbox = draw.textbbox((0, 0), line, font=fonts['body'])
text_width = bbox[2] - bbox[0]
x = (SLIDE_SIZE[0] - text_width) // 2
y = subtitle_y + (i * 45)
draw.text((x, y), line, fill=hex_to_rgb(COLORS['secondary']), font=fonts['body'])
return img
def create_content_slide(slide_data: Dict, fonts: Dict, account: int, photo_path: str = None) -> Image.Image:
"""Create standard content slide"""
img = Image.new('RGB', SLIDE_SIZE, hex_to_rgb(COLORS['neutral_light']))
draw = ImageDraw.Draw(img)
current_y = MARGIN
# Draw title
if slide_data.get('title'):
title = slide_data['title']
# Remove bullet points if present
title = title.lstrip('•- ').strip()
title_lines = wrap_text(title, fonts['subtitle'], CONTENT_WIDTH, draw)
for line in title_lines:
draw.text((MARGIN, current_y), line,
fill=hex_to_rgb(COLORS['primary']), font=fonts['subtitle'])
current_y += 60
current_y += 20 # Space after title
# Draw body text
if slide_data.get('body'):
body = slide_data['body']
# Handle bullet points
body_lines = body.split('\n')
max_content_y = SLIDE_SIZE[1] - FOOTER_HEIGHT - MARGIN
for line in body_lines:
if current_y > max_content_y:
break
line = line.strip()
if not line:
current_y += 20
continue
# Check for bullet points
is_bullet = line.startswith(('•', '-', '*'))
if is_bullet:
line = line.lstrip('•-* ').strip()
# Draw bullet point
draw.ellipse([(MARGIN, current_y + 10), (MARGIN + 10, current_y + 20)],
fill=hex_to_rgb(COLORS['accent']))
text_x = MARGIN + 25
else:
text_x = MARGIN
# Wrap and draw text
wrapped_lines = wrap_text(line, fonts['body'], CONTENT_WIDTH - (25 if is_bullet else 0), draw)
for wrapped_line in wrapped_lines:
if current_y > max_content_y:
break
draw.text((text_x, current_y), wrapped_line,
fill=hex_to_rgb(COLORS['neutral_dark']), font=fonts['body'])
current_y += 45
current_y += 10 # Space between paragraphs
# Draw footer
draw_footer(draw, fonts, account, photo_path)
return img
def generate_carousel(input_file: str, account: int, output_dir: str = None,
photo_path: str = None, max_slides: int = 10):
"""
Main function to generate carousel slides
Args:
input_file: Path to input content file
account: Account number (1 or 2)
output_dir: Directory to save slides (default: skill output dir)
photo_path: Path to profile photo
max_slides: Maximum number of slides to generate
"""
# Setup output directory
if output_dir is None:
base_name = Path(input_file).stem
output_dir = OUTPUT_DIR / "carousels" / base_name / f"account-{account}"
os.makedirs(output_dir, exist_ok=True)
# Default photo path
if photo_path is None:
photo_path = KNOWLEDGE_BASE / "brand" / "photo-shailesh.jpg"
# Load fonts
fonts = load_fonts()
# Parse content
slides_data = parse_content(input_file)
# Limit slides
if len(slides_data) > max_slides:
slides_data = slides_data[:max_slides]
print(f"Note: Limited to {max_slides} slides")
print(f"Generating {len(slides_data)} slides for {ACCOUNTS[account]}...")
# Generate slides
for i, slide_data in enumerate(slides_data, 1):
slide_type = slide_data.get('type', 'content')
if i == 1 or slide_type == 'title':
img = create_title_slide(slide_data, fonts, account, photo_path)
else:
img = create_content_slide(slide_data, fonts, account, photo_path)
# Save slide
output_path = os.path.join(output_dir, f"slide-{i:02d}.png")
img.save(output_path, quality=95)
print(f" Created: {output_path}")
print(f"\nCarousel complete! {len(slides_data)} slides saved to {output_dir}")
return output_dir
def main():
parser = argparse.ArgumentParser(
description='Generate Instagram carousel slides for Dr. Shailesh Singh'
)
parser.add_argument('input_file', help='Input content file (text, JSON, or markdown)')
parser.add_argument('account', type=int, choices=[1, 2],
help='Account number: 1=@heartdocshailesh, 2=@dr.shailesh.singh')
parser.add_argument('-o', '--output', help='Output directory')
parser.add_argument('-p', '--photo', help='Path to profile photo')
parser.add_argument('-m', '--max-slides', type=int, default=10,
help='Maximum number of slides (default: 10)')
args = parser.parse_args()
if not os.path.exists(args.input_file):
print(f"Error: Input file not found: {args.input_file}")
sys.exit(1)
try:
generate_carousel(
input_file=args.input_file,
account=args.account,
output_dir=args.output,
photo_path=args.photo,
max_slides=args.max_slides
)
except Exception as e:
print(f"Error generating carousel: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()