Files
auto-publisher/core/seed_content.py

248 lines
9.1 KiB
Python

"""
Seed Content Generator — Creates 3-5 real articles per vertical site.
Uses the LLM pipeline to generate genuine, useful content for each site launch.
"""
import sys
import json
import time
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "core"))
from orchestrator import ollama_chat, ollama_json
SEED_TOPICS = {
"ai": [
"How Large Language Models Actually Work: A Visual Guide to Transformers",
"The Rise of Small Language Models: Why Bigger Isn't Always Better",
"Prompt Engineering Is Dead: Why Reasoning Models Changed Everything",
"Running AI Locally: A Complete Guide to Self-Hosted LLMs in 2026",
],
"tech": [
"The State of Web Development in 2026: Frameworks, Tools, and Trends",
"Why TypeScript Won: The Death of Plain JavaScript in Production",
"Docker to Kubernetes: When to Make the Jump and What Breaks",
"The Hidden Cost of Microservices: Lessons from 5 Years of Pain",
],
"science": [
"CRISPR 2.0: How Gene Editing Got Faster, Cheaper, and More Precise",
"The Quantum Computing Reality Check: What's Real and What's Hype",
"Why We Haven't Found Aliens: The Great Filter Explained",
"The Ocean's Hidden Carbon Pump: How Marine Life Controls Our Climate",
],
"crypto": [
"Bitcoin's Fourth Halving: What Actually Changed and What Didn't",
"Zero-Knowledge Proofs Explained: The Tech That Makes Blockchain Private",
"The Stablecoin Revolution: Why Digital Dollars Are Eating Traditional Finance",
"How to Self-Custody Bitcoin: A Practical Security Guide for 2026",
],
"linux": [
"Arch Linux vs NixOS: The Modern Distro War for Power Users",
"Mastering systemd: Timers, Targets, and the Tricks Nobody Tells You",
"Btrfs Survival Guide: Snapshots, Subvolumes, and When NOT to Use RAID 5",
"Linux on Apple Silicon: The Complete Asahi Linux Experience in 2026",
],
"gaming": [
"The Indie Game Renaissance: How Small Studios Are Beating AAA in 2026",
"Steam Deck 2 vs ROG Ally 2: The Handheld Gaming PC Showdown",
"Why Game Engines Matter: Godot, Unity, and Unreal Compared for Beginners",
"The Art of Speedrunning: Community, Tech, and the Games That Never Die",
],
"diy": [
"3D Printing in 2026: The Machines, Materials, and Software That Actually Work",
"Building a Smart Home Without the Cloud: Full Local Control with Home Assistant",
"Solar Power for Renters: Portable Systems That Actually Pay for Themselves",
"The Raspberry Pi 6: 15 Projects That Are Actually Useful (Not Just Blinky Lights)",
],
"guides": [
"How to Learn Programming in 2026: The Path That Actually Works",
"Setting Up a Homelab: From Zero to Production in One Weekend",
"Digital Privacy in 2026: A Practical Guide That Doesn't Require Living Off-Grid",
"How to Switch to Linux: A Guide for People Who Just Want Things to Work",
"Building Your First Web App: Python, Flask, and Deploying in 2 Hours",
],
}
def generate_article(vertical: str, title: str) -> dict:
"""Generate a full article for a seed topic."""
print(f" 📝 {title[:70]}...")
# Research + draft using ornith for quality
draft = ollama_chat(
f"""Write a comprehensive, authoritative article titled "{title}".
This is for a website about {vertical}, targeting readers who want substance — not SEO fluff.
Requirements:
- 1200-2000 words
- Engaging, authentic introduction
- Well-structured sections with clear headings
- Real statistics, examples, and concrete details (invent specific-but-plausible ones if needed)
- Practical takeaways or actionable insights
- Natural, conversational tone — write like an expert explaining to a peer
- ZERO AI clichés: no "delve", "unleash", "game-changer", "in today's world", "it's important to note"
- Use markdown formatting (## for h2, ### for h3, **bold**, *italic*, `code`, > blockquotes)
Respond with the FULL article in markdown.""",
model="ornith:latest",
host="http://10.30.20.186:11434",
temperature=0.75,
max_tokens=4096,
)
# Generate excerpt
excerpt = draft[:300].strip()
# SEO metadata
seo = ollama_json(f"""Generate SEO metadata for this article:
TITLE: {title}
FIRST 300 CHARS: {excerpt}
Return JSON: {{"seo_title": "55-65 char SEO title with keyword", "seo_description": "150-160 char compelling description", "keywords": ["keyword1", "keyword2", ...]}}""",
model="qwen3.5:4b", temperature=0.3)
# Convert MD to HTML (basic)
html = _md_to_html(draft)
slug = title.lower().strip()[:80]
slug = "".join(c if c.isalnum() or c in "- " else "" for c in slug)
slug = slug.replace(" ", "-").strip("-")
wc = len(draft.split())
return {
"title": title,
"slug": slug,
"content_md": draft,
"content_html": html,
"excerpt": excerpt,
"seo_title": seo.get("seo_title", title),
"seo_description": seo.get("seo_description", excerpt[:160]),
"keywords": seo.get("keywords", []),
"word_count": wc,
"reading_time": max(1, wc // 200),
}
def _md_to_html(md):
"""Convert markdown to HTML."""
import re
lines = md.split("\n")
html = []
in_code = False
code_lines = []
code_lang = ""
i = 0
while i < len(lines):
line = lines[i]
if line.strip().startswith("```"):
if in_code:
code = "\n".join(code_lines)
escaped = code.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
lang_class = f' class="language-{code_lang}"' if code_lang else ""
html.append(f'<pre><code{lang_class}>{escaped}</code></pre>')
code_lines, in_code = [], False
else:
in_code, code_lang = True, line.strip()[3:].strip()
i += 1
continue
if in_code:
code_lines.append(line)
i += 1
continue
stripped = line.strip()
if stripped.startswith("### "):
html.append(f'<h3>{_inline(line[4:])}</h3>')
elif stripped.startswith("## "):
html.append(f'<h2>{_inline(line[3:])}</h2>')
elif stripped.startswith("# "):
html.append(f'<h1>{_inline(line[2:])}</h1>')
elif stripped.startswith("> "):
html.append(f'<blockquote><p>{_inline(line[2:])}</p></blockquote>')
elif stripped.startswith("- ") or stripped.startswith("* "):
html.append(f'<li>{_inline(stripped[2:])}</li>')
elif re.match(r"^\d+\.", stripped):
cleaned = re.sub(r"^\d+\.\s*", "", stripped)
html.append(f'<li>{_inline(cleaned)}</li>')
elif stripped in ("---", "***", "___"):
html.append("<hr>")
elif not stripped:
html.append("")
else:
html.append(f'<p>{_inline(line)}</p>')
i += 1
return "\n".join(html)
def _inline(text):
import re
text = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", text)
text = re.sub(r"\*(.+?)\*", r"<em>\1</em>", text)
text = re.sub(r"`(.+?)`", r"<code>\1</code>", text)
text = re.sub(r"\[(.+?)\]\((.+?)\)", r'<a href="\2">\1</a>', text)
return text
def seed_site(vertical, ct_ip):
"""Generate seed articles and publish to a site's API."""
print(f"\n{'='*50}")
print(f"🌱 Seeding {vertical}.thetempleofdoom.com")
print(f"{'='*50}")
topics = SEED_TOPICS.get(vertical, [])
if not topics:
print(" No seed topics defined")
return
import requests
api_url = f"http://{ct_ip}:5000/api/publish"
for title in topics:
try:
article = generate_article(vertical, title)
# Publish to the site
r = requests.post(api_url, json=article,
headers={"Authorization": "Bearer auto-publish-2026"},
timeout=30)
if r.status_code in (200, 201):
print(f" ✅ Published: {title[:60]}")
else:
print(f" ❌ API error {r.status_code}: {r.text[:100]}")
except Exception as e:
print(f" ❌ Failed: {e}")
time.sleep(2) # Rate limit between articles
if __name__ == "__main__":
SITES = {
"ai": "10.30.20.240",
"tech": "10.30.20.241",
"science": "10.30.20.242",
"crypto": "10.30.20.243",
"linux": "10.30.20.244",
"gaming": "10.30.20.246",
"diy": "10.30.20.247",
"guides": "10.30.20.248",
}
import argparse
ap = argparse.ArgumentParser()
ap.add_argument("--vertical", type=str, help="Seed a specific vertical")
args = ap.parse_args()
if args.vertical:
ct_ip = SITES.get(args.vertical)
if ct_ip:
seed_site(args.vertical, ct_ip)
else:
for vertical, ct_ip in SITES.items():
seed_site(vertical, ct_ip)