Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 150 additions & 0 deletions recipes/infographic-generator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# Infographic Generator

*Turn research docs and Open Brain thoughts into professional infographic images*

Generate infographic images from any markdown document, research analysis, or Open Brain thought cluster using Gemini's free-tier image generation API. Auto-chunks content into logical segments, writes verbose image prompts, generates PNGs, and optionally captures prompts back to Open Brain for future retrieval.

## What It Does

Takes structured or unstructured content and runs a four-step process: **Analyze** the source for logical segments, **Chunk** into one-infographic-per-topic, **Prompt** with verbose image generation instructions (300+ words each with specific colors, layout, typography), and **Generate** actual PNG images via Gemini API. Prompts can be stored in Open Brain for future regeneration or adaptation.

## Prerequisites

- Working Open Brain setup ([guide](../../docs/01-getting-started.md))
- Claude Code (or another AI coding tool that supports skills/system prompts)
- Open Brain MCP tools connected (`capture_thought`, `search_thoughts`)
- Python 3.10+ installed
- Gemini API key (free tier, get one at [ai.google.dev](https://ai.google.dev))

### Credential Tracker

```
From your existing Open Brain setup:
- Project URL: _______________
- Open Brain MCP server connected: yes / no

New for this recipe:
- Gemini API key: _______________ (from ai.google.dev, free tier works)
```

## Steps

### 1. Create the skill directory

```bash
mkdir -p ~/.claude/skills/infographic-generator
```

### 2. Copy the skill and generation script

Copy `infographic-generator.skill.md` and `generate.py` from this recipe:

```bash
cp infographic-generator.skill.md ~/.claude/skills/infographic-generator/SKILL.md
cp generate.py ~/.claude/skills/infographic-generator/generate.py
chmod +x ~/.claude/skills/infographic-generator/generate.py
```

### 3. Set up the Python environment

```bash
cd ~/.claude/skills/infographic-generator
python3 -m venv .venv
source .venv/bin/activate
pip install google-genai Pillow
```

### 4. Set your Gemini API key

```bash
export GEMINI_API_KEY="your-api-key-here"
```

Or add it to your shell profile for persistence:

```bash
echo 'export GEMINI_API_KEY="your-api-key-here"' >> ~/.zshrc
```

### 5. Verify Claude Code picks up the skill

Restart Claude Code. To verify, say "make an infographic from this doc" or "visualize this research" and confirm it references the Infographic Generator methodology.

### 6. Generate your first infographic

Point the skill at any markdown file:

```
Make infographics from docs/research/my-analysis.md
```

Or use Open Brain as the source:

```
Visualize my thoughts about [topic]
```

The skill will:
1. Read and analyze the source content
2. Chunk into logical segments (one infographic per major topic)
3. Write verbose prompts with specific hex colors, layout types, and typography
4. Save prompts to `./infographic-prompts/`
5. Generate PNG images via Gemini API to `./media/`

### 7. Review and iterate

After generation, the skill shows each image. You can:

```
redo 3 # regenerate infographic #3
redo 3 premium # regenerate with higher quality model
```

### 8. (Optional) Capture prompts to Open Brain

The generated prompts are 300+ word rich text documents. Storing them in Open Brain makes them searchable across sessions:

```
Save these infographic prompts to Open Brain
```

Future sessions can find them: `search_thoughts("infographic ROI analysis")`

## Expected Outcome

When working correctly, you should see:

- A prompt file saved to `./infographic-prompts/[source]-prompts.md` with one verbose prompt per content segment
- PNG images saved to `./media/` with descriptive filenames
- A manifest file at `./media/_latest_generation.json` linking filenames to titles
- Each prompt includes specific hex colors, aspect ratio, layout type, typography specs, and "what to avoid" guidance

A typical 2-page research doc yields 4-8 infographics. Generation takes 30-60 seconds per image on Gemini's free tier.

## Open Brain Integration

This recipe connects to Open Brain in two ways:

**As a source (read side):** Search Open Brain for thoughts on a topic, synthesize them, then generate infographics from the synthesis. "Visualize my thoughts about MSP operations" pulls relevant thoughts and creates visuals from them.

**As storage (write side):** Generated prompts can be captured back to Open Brain. This creates a searchable library of visual templates. "What infographic did I make for the deal structure?" returns the prompt, which can be regenerated or adapted for new content.

Together with [Panning for Gold](../panning-for-gold/) and Auto-Capture Protocol (PR [#42](https://github.com/NateBJones-Projects/OB1/pull/42)), this forms a complete capture-process-visualize flywheel (see [Issue #84](https://github.com/NateBJones-Projects/OB1/issues/84)).

## Troubleshooting

**Issue:** `GEMINI_API_KEY not set` error when generating.
**Solution:** Ensure the environment variable is exported in your current shell session. Run `echo $GEMINI_API_KEY` to verify. If using Claude Code, the shell inherits from your profile, so add it to `~/.zshrc` or `~/.bashrc`.

**Issue:** Gemini returns content policy errors for certain prompts.
**Solution:** Some prompts trigger safety filters (surveillance themes, medical imagery, etc.). The script retries once automatically. If it still fails, the prompt is logged and skipped. You can manually edit the prompt to soften the language and use `--redo N` to regenerate just that image.

**Issue:** Images have poor text rendering.
**Solution:** Gemini's free tier (Flash) struggles with small text in infographics. Use `--premium` flag for better text rendering (uses a higher-quality model, may require a paid API key depending on your Gemini plan).

**Issue:** Too many infographics generated from a short document.
**Solution:** The skill chunks aggressively by default (one per section/topic). For shorter docs, tell the skill: "Make 3 infographics max from this doc" to constrain output.

---

*Part of the [Open Brain Flywheel](https://github.com/NateBJones-Projects/OB1/issues/84): capture, process, visualize.*
171 changes: 171 additions & 0 deletions recipes/infographic-generator/generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""
Infographic image generator using Google Gemini API.
Called by the infographic-generator skill after prompts are written.

Usage:
python3 generate.py <prompts-file.md> [--output-dir ./media] [--premium] [--redo N]

Environment:
GEMINI_API_KEY - Required. Get a free key at https://ai.google.dev

Flags:
--premium Use higher-quality model (may require paid API key)
--redo N Regenerate only infographic #N from the file
"""

import sys
import os
import re
import argparse
from pathlib import Path

# Use the skill's venv if available
VENV_SITE = Path(__file__).parent / ".venv" / "lib"
for p in VENV_SITE.glob("python*/site-packages"):
if str(p) not in sys.path:
sys.path.insert(0, str(p))

from google import genai
from google.genai import types

API_KEY = os.environ.get("GEMINI_API_KEY", "")
if not API_KEY:
print("ERROR: GEMINI_API_KEY environment variable is not set.")
print("Get a free key at https://ai.google.dev")
sys.exit(1)

MODEL_FREE = "gemini-2.5-flash-image"
MODEL_PREMIUM = "gemini-3.1-flash-image-preview"


def extract_prompts(md_path):
"""Extract prompt blocks from the markdown file."""
text = Path(md_path).read_text()

# Split on "## Infographic" headers
sections = re.split(r"(?=## Infographic \d+)", text)
prompts = []

for section in sections:
# Find the title
title_match = re.match(r'## Infographic \d+:\s*["\']?(.+?)["\']?\s*$', section, re.MULTILINE)
if not title_match:
continue
title = title_match.group(1).strip().strip('"\'')

# Find the prompt content (everything after "### Prompt" line)
prompt_match = re.search(r'### Prompt.*?\n\n(.+?)(?=\n---|\n## |\Z)', section, re.DOTALL)
if not prompt_match:
continue

prompt_text = prompt_match.group(1).strip()

# Extract aspect ratio if specified
ar_match = re.search(r'Aspect ratio:\s*(\d+:\d+)', section)
aspect_ratio = ar_match.group(1) if ar_match else "4:5"

# Clean the title for filename
safe_name = re.sub(r'[^\w\s-]', '', title.lower())
safe_name = re.sub(r'[\s]+', '-', safe_name).strip('-')[:60]

prompts.append({
"title": title,
"prompt": prompt_text,
"aspect_ratio": aspect_ratio,
"filename": safe_name + ".png",
})

return prompts


def generate_image(client, model, prompt_data):
"""Generate a single infographic image."""
response = client.models.generate_content(
model=model,
contents=prompt_data["prompt"],
config=types.GenerateContentConfig(
response_modalities=["IMAGE"],
image_config=types.ImageConfig(
aspect_ratio=prompt_data["aspect_ratio"],
),
),
)

for part in response.parts:
if part.inline_data:
return part.as_image()

return None


def main():
parser = argparse.ArgumentParser(description="Generate infographic images from prompt markdown")
parser.add_argument("prompts_file", help="Path to the prompts markdown file")
parser.add_argument("--output-dir", default="./media", help="Output directory for images")
parser.add_argument("--premium", action="store_true", help="Use higher-quality model (may require paid key)")
parser.add_argument("--redo", type=int, help="Regenerate only infographic #N")
args = parser.parse_args()

model = MODEL_PREMIUM if args.premium else MODEL_FREE
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)

print(f"Model: {model} ({'premium' if args.premium else 'free tier'})")
print(f"Output: {output_dir}")

prompts = extract_prompts(args.prompts_file)
if not prompts:
print("ERROR: No prompts found in file.")
print("Expected format: '## Infographic N: Title' followed by '### Prompt' section")
sys.exit(1)

print(f"Found {len(prompts)} prompts")

# Filter to single prompt if --redo
if args.redo:
idx = args.redo - 1
if idx < 0 or idx >= len(prompts):
print(f"ERROR: --redo {args.redo} out of range (1-{len(prompts)})")
sys.exit(1)
prompts = [prompts[idx]]
print(f"Regenerating only: #{args.redo} - {prompts[0]['title']}")

client = genai.Client(api_key=API_KEY)

results = []
for i, p in enumerate(prompts):
num = args.redo if args.redo else i + 1
progress_num = i + 1
print(f"\n[{progress_num}/{len(prompts)}] Generating: {p['title']}")
print(f" Aspect ratio: {p['aspect_ratio']}")

try:
image = generate_image(client, model, p)
if image:
out_path = output_dir / p["filename"]
image.save(str(out_path))
print(f" SAVED: {out_path}")
results.append({"file": p["filename"], "caption": p["title"], "path": str(out_path)})
else:
print(f" FAILED: No image in response")
except Exception as e:
print(f" ERROR: {e}")

# Print summary
print(f"\n{'='*50}")
print(f"Generated {len(results)}/{len(prompts)} images")
for r in results:
print(f" {r['file']}")

# Output JSON manifest for the skill to consume
if results:
import json
manifest_path = output_dir / "_latest_generation.json"
with open(manifest_path, "w") as f:
json.dump(results, f, indent=2)
print(f"\nManifest: {manifest_path}")


if __name__ == "__main__":
main()
Loading
Loading