AI Content Generation Engine — turn a single topic into a publish-ready blog post, then repurpose it for SEO and social media.
A production-quality Python CLI that goes from topic → markdown in one command, with built-in SEO scoring, tone rewrites, and social-media repurposing. Built to demonstrate clean architecture, prompt engineering discipline, and UX polish.
Most "AI blog generator" demos stop at "ask the model for a post." SCRIBE goes further:
- Real SEO scoring, not AI-hallucinated numbers — readability via
textstat, keyword density from actual counts, structure heuristics. The model only fills the qualitative gaps. - Single-responsibility modules behind one API wrapper, so swapping OpenAI for Anthropic is a one-file change.
- A real CLI with Rich panels, spinners, tables, and a
--no-exportescape hatch — not a notebook. - Typed tone rewrites with a Typer enum that rejects bad input before the API call.
It's a portfolio piece: the code is the resume.
scribe title— generatenvaried title candidates (list / how-to / question / contrarian angles)scribe outline— produce a structured[{section, description}, ...]outlinescribe generate— full pipeline: title → outline → draft → export to Markdown with YAML frontmatterscribe seo— real computed score (readability 40% / keyword density 30% / structure 30%) + AI meta description + 3 suggestionsscribe improve— rewrite for clarity, flow, and engagement; renders a unified diffscribe tone <professional|casual|witty|technical|persuasive>— tone-shift rewrites with a Typer enumscribe social— LinkedIn post, X/Twitter thread (1/,2/, ...), Facebook post — three Rich panels;--savewrites each tooutput/social/scribe history— Rich table of past generations- Friendly errors (Rich panels, no stack traces) for missing keys, empty topics, bad tones, API hiccups
git clone https://github.com/<you>/scribe.git
cd scribe
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env
# edit .env and set OPENAI_API_KEYWindows users: set PYTHONIOENCODING=utf-8 once per shell to keep Typer's Rich help (it uses →) from crashing the legacy cp1252 stdout.
scribe title --topic "AI startup ideas" --count 5
scribe outline --topic "AI startup ideas" --sections 6
scribe generate --topic "AI startup ideas" --tone casual --length 1500
scribe generate --topic "AI startup ideas" --no-export # print only
scribe seo --file output/ai-startup-ideas.md --keywords "AI startup, founder"
scribe improve --file output/ai-startup-ideas.md
scribe tone casual --file output/ai-startup-ideas.md
scribe tone technical --text "Paste draft here..."
scribe social --file output/ai-startup-ideas.md
scribe social --file output/ai-startup-ideas.md --save # write to output/social/
scribe history
scribe --versionIf you run scribe with no args you get a small ASCII banner and a quick-start hint.
____ ___ ____ ___ ___ __ __
/ ___|| _ \/ ___|_ _|_ _|| \/ |
\___ \| _/\___ \| | | | | |\/| |
___) | | ___) | | | | | | | |
|____/|_| |____/___|___| |_| |_|
SCRIBE v0.1.0 · AI content generation engine
Every module has one job. Every API call goes through client.py so swapping providers is a single-file change.
| Module | Responsibility |
|---|---|
main.py |
Typer app, command registration, banner, error panels |
client.py |
OpenAI wrapper (chat_completion() w/ exponential backoff) — the only module that talks to the API |
prompts.py |
All system/user prompt templates — no API calls live here |
generator.py |
generate_titles(), generate_draft() |
outlines.py |
generate_outline() |
seo.py |
analyze_seo() — computed score + AI-suggested meta/suggestions |
improver.py |
improve_content(), change_tone() |
social.py |
generate_social_posts() → {linkedin, twitter_thread, facebook} |
exporter.py |
export_markdown() — YAML frontmatter + slugified filename |
config.py |
Env loading, defaults, paths |
utils.py |
Shared Rich console, with_spinner(), slugify, history I/O |
score = round(
readability_score * 0.40
+ keyword_score * 0.30 (skipped if no --keywords; weight redistributed)
+ structure_score * 0.30
)
- Readability (40%):
textstat.flesch_reading_ease(text)— clamped to 0-100 - Keyword density (30%): per-keyword
count / total_words * 100; 1-2% ideal, 0.5-1% or 2-3% acceptable, <0.5% low, >3% stuffed - Structure (30%): H2 headings + intro paragraph + conclusion section + paragraph-length distribution
The AI is asked for exactly two qualitative outputs: a meta description (150-160 chars) and three concrete improvement suggestions. It is never asked for the score.
| Env var | Default | Purpose |
|---|---|---|
OPENAI_API_KEY |
(required) | Your OpenAI API key |
SCRIBE_MODEL |
gpt-4o-mini |
Any chat-completions-compatible model |
.env is gitignored. If the key is missing, SCRIBE prints a Rich error panel and exits 1 — never a stack trace.
| Situation | Behavior |
|---|---|
Missing OPENAI_API_KEY |
Rich red panel with setup steps, exit 1 |
Empty / too-short --topic |
Friendly yellow panel asking for more detail, exit 1 |
Invalid --tone |
Typer auto-rejects with valid options listed, exit 2 |
| OpenAI rate-limit / network error | Retried 3× with exponential backoff (client.py); clear Rich panel if all fail |
| AI returns malformed JSON for SEO suggestions | Falls back to a deterministic truncated meta; the score (computed) is unaffected |
- SQLite history with full-text search, tags, and per-topic stats
- LangChain orchestration for prompt chaining and tool use
- Vector DB style-matching — pull the top 3 past posts in a similar tone and use them as few-shot examples
- Template library — load user-defined prompt templates from disk
- Streaming responses for live diff rendering in
improveandtone - Concurrent social generation with bounded concurrency (3 calls today run sequentially)
# Verify the CLI
python -m scribe.main --help
python -m scribe.main version --help # not a thing; use --version
# Manual smoke test of a single command without hitting the API
PYTHONIOENCODING=utf-8 python -m scribe.main seo --file output/<some>.mdThe v2 hook: comments in config.py, prompts.py, and client.py mark the seams where new pieces slot in.
MIT — see LICENSE.
The .gitignore excludes output/*.md by default so generated content doesn't pollute commits. To produce a real screenshot for the README, run something like:
scribe generate --topic "Starting an AI business in 2026" --tone professional --length 1500
scribe seo --file output/starting-an-ai-business-in-2026.md --keywords "AI business"Then commit the resulting .md file manually with git add -f output/<file>.md for the portfolio screenshot.