248 lines
9.1 KiB
Python
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-mlx", 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("&", "&").replace("<", "<").replace(">", ">")
|
|
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)
|