SCRIBE
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.
Why this exists
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.
Features
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 suggestions; keywords auto-detected via NLTK when--keywordsis omittedscribe 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 pdf— export a Markdown post to a clean, single-column PDFscribe import-url— extract an article withnewspaper4kand runimprove,social, orseoon itscribe history— Rich table of past generations- Friendly errors (Rich panels, no stack traces) for missing keys, empty topics, bad tones, API hiccups
Installation
From PyPI (recommended)
pip install scribe-gen
From source
git clone https://github.com/khizerarain/scribe.git
cd scribe
pip install -e .
Create a .env file in your working directory and add your OpenAI key:
cp .env.example .env
# edit .env and set OPENAI_API_KEY
Windows users: set PYTHONIOENCODING=utf-8 once per shell to keep Typer's Rich help (it uses →) from crashing the legacy cp1252 stdout.
Usage
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 pdf --file output/ai-startup-ideas.md --output outputs/
scribe import-url https://example.com/article --action social
scribe history
scribe --version
If you run scribe with no args you get a small ASCII banner and a quick-start hint.
Example output
____ ___ ____ ___ ___ __ __
/ ___|| _ \/ ___|_ _|_ _|| \/ |
\___ \| _/\___ \| | | | | |\/| |
___) | | ___) | | | | | | | |
|____/|_| |____/___|___| |_| |_|
SCRIBE v0.1.0 · AI content generation engine
Architecture
Every module has one job. Every API call goes through client.py so swapping providers is a single-file change.
| Module | Responsibility |
|---|---|
cli.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 |
SEO scoring formula
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.
Configuration
| 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.
Error handling
| 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 |
Roadmap (v2 — seams are in place, not implemented)
- 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)
Development
# Verify the CLI
python -m scribe.cli --help
python -m scribe.cli --version
# Manual smoke test of a single command without hitting the API
PYTHONIOENCODING=utf-8 python -m scribe.cli seo --file output/<some>.md
The v2 hook: comments in config.py, prompts.py, and client.py mark the seams where new pieces slot in.
License
MIT — see LICENSE.
Sample output
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.
Metadata
Release files for scribe-gen 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| scribe_gen-0.1.0.tar.gz | 48.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| scribe_gen-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 76.3 kB
Release files / scribe_gen-0.1.0.tar.gz
| Download URL | scribe_gen-0.1.0.tar.gz |
|---|---|
| Size | 48.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
755325be51bdbc0d47bdb31a4218b10851aafa067c02095f77cc35a5da432ab8
|
|
BLAKE2b-256 checksum How to use checksums |
5a39a2db938df4faf39ade37c415caa71d597df96d2fadc2ba3ba90264d65039
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|
Release files / scribe_gen-0.1.0-py3-none-any.whl
| Download URL | scribe_gen-0.1.0-py3-none-any.whl |
|---|---|
| Size | 28.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
0d40f765a31888e5e84493445b2a822b1eb7dbc389fa8b7259553242c0dc6554
|
|
BLAKE2b-256 checksum How to use checksums |
614e2f14f3dec0b828e4caa2b0ed5b1122190a65121c2ecc7b2aeab36ac8f43e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.13.7
|