Initial commit: site code, layouts, content (images added separately)

Fixes duplicate placeholder cover image on the 3 newest blog posts,
broken aspect-ratio classes on the albums listing page, and a
non-responsive fixed sidebar on the chat page.
This commit is contained in:
Dr. Jones
2026-07-08 06:18:17 +00:00
commit d9961a76ad
80 changed files with 9438 additions and 0 deletions

320
scripts/generate_blog_post.py Executable file
View File

@@ -0,0 +1,320 @@
#!/usr/bin/env python3
"""
Ollama Blog Post Generator
This script generates a new blog post using Ollama's drjones-posts-to-much model
and adds it to the Hugo blog. It runs automatically every 3 days via systemd timer.
The script uses random prompt variations to ensure each blog post is unique.
The model generates titles with credentials, which are automatically extracted
and separated from the article content.
Usage:
python3 generate_blog_post.py
Configuration:
- Ollama endpoint: 10.30.20.110:11434
- Model: drjones-posts-to-much
- Blog directory: /root/hydro-sterile/content/blog/
Error Handling:
If the Ollama endpoint is down, the script fails quietly (exits with code 0)
to prevent systemd from logging errors. It will retry in 3 days.
Author: Dr. Jones
"""
import os
import sys
import json
import random
from datetime import datetime, timedelta
from pathlib import Path
try:
from ollama import Client
except ImportError:
print("ERROR: ollama Python library not installed. Run: pip3 install ollama", file=sys.stderr)
sys.exit(1)
# Configuration
OLLAMA_HOST = "http://10.30.20.110:11434"
MODEL = "drjones-posts-to-much"
BLOG_DIR = Path("/root/hydro-sterile/content/blog")
SITE_DIR = Path("/root/hydro-sterile")
# System prompt for generating blog posts
SYSTEM_PROMPT = """You are Dr. Jones, a sterile hydroponics expert with 20 years of experience growing cannabis in a legal state.
Your writing style is authentic, direct, and knowledgeable. You focus on sterile hydroponics methodology,
growing techniques, and practical advice. Write blog posts that are:
- 300-500 words
- Focused on sterile hydroponics, cannabis cultivation, or related topics
- Written in first person (I, me, my)
- Practical and actionable
- Authentic and conversational
The model will generate a title with credentials, then the article content."""
def get_next_post_number():
"""Find the next available post number by checking existing posts."""
existing_posts = list(BLOG_DIR.glob("post-*.md"))
if not existing_posts:
return 1
numbers = []
for post in existing_posts:
try:
# Extract number from filename like "post-1.md"
num = int(post.stem.split("-")[1])
numbers.append(num)
except (ValueError, IndexError):
continue
return max(numbers) + 1 if numbers else 1
def generate_filename(post_number):
"""Generate the filename for a new post."""
return BLOG_DIR / f"post-{post_number}.md"
def create_frontmatter(title, summary, image_path=None):
"""Create YAML frontmatter for Hugo blog post."""
date_str = datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
frontmatter = f"""---
title: "{title}"
date: {date_str}
image: "{image_path or '/images/IMG_1767.jpeg'}"
summary: "{summary}"
---
"""
return frontmatter
def generate_random_prompt():
"""Generate a random prompt variation to ensure different content each time."""
# Different ways to ask for hydroponics knowledge
prompt_variations = [
"Share your wisdom about sterile hydroponics. What insights can you offer growers today?",
"I'm seeking your expertise on hydroponic cultivation. What knowledge can you impart?",
"Please share your knowledge about sterile hydroponics systems. What should growers know?",
"I need your guidance on hydroponic growing. What can you teach me today?",
"What hydroponic wisdom can you share? I'm eager to learn from your experience.",
"Please enlighten me about sterile hydroponics. What key insights do you have?",
"I'm asking for your hydroponic expertise. What valuable knowledge can you provide?",
"Share your hydroponic knowledge with me. What should I know about sterile systems?",
"I'm seeking your hydroponic wisdom. What can you teach growers today?",
"Please provide your insights on sterile hydroponics. What's important to know?",
"What can you tell me about hydroponic cultivation? I'm looking for your expertise.",
"I need your hydroponic knowledge. What guidance can you offer?",
"Share your experience with sterile hydroponics. What should growers understand?",
"I'm asking for your hydroponic insights. What knowledge can you share?",
"Please enlighten me about hydroponic systems. What wisdom do you have?",
]
# Additional random elements to add variety
topic_hints = [
"",
" Focus on practical techniques.",
" Emphasize sterile methodology.",
" Include real-world examples.",
" Cover common mistakes to avoid.",
" Discuss nutrient management.",
" Talk about root health.",
" Share system design tips.",
" Address problem-solving.",
" Include cultivation tips.",
]
# Random opening phrases
openings = [
"",
"Today, ",
"In this post, ",
"Let me share ",
"Here's what I know: ",
]
base_prompt = random.choice(prompt_variations)
topic_hint = random.choice(topic_hints)
opening = random.choice(openings)
# Combine with some randomness
full_prompt = opening + base_prompt + topic_hint
return full_prompt
def call_ollama():
"""Call Ollama API to generate blog post content."""
client = Client(host=OLLAMA_HOST)
try:
# Generate random prompt for variety
user_prompt = generate_random_prompt()
# Generate the blog post content
response = client.chat(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_prompt}
],
options={
"temperature": 0.8 + random.uniform(0, 0.2), # Random temperature between 0.8-1.0 for more variety
"top_p": 0.9,
"repeat_penalty": 1.5, # High repeat penalty to avoid repetitive content
}
)
content = response.message.content
return content
except Exception as e:
# Fail quietly - don't raise exception, just return None
# This allows the script to exit gracefully when Ollama is down
print(f"Warning: Could not connect to Ollama at {OLLAMA_HOST}: {e}", file=sys.stderr)
return None
def extract_title_and_summary(content):
"""Extract title and summary from generated content.
The model generates title with credentials, then the article.
We need to separate these and extract just the title and article content.
"""
lines = content.strip().split('\n')
# The model typically generates: "Title - Dr. Jones, Credentials\n\nArticle content"
# Or: "Title\nDr. Jones, Credentials\n\nArticle content"
# Or: "# Title\n\nDr. Jones, Credentials\n\nArticle content"
title = ""
article_start_idx = 0
credentials_found = False
# Look for title (usually first line or first few lines)
# Title might be followed by credentials line, then blank line, then article
for i, line in enumerate(lines):
line_stripped = line.strip()
# Skip empty lines at the start
if not line_stripped:
continue
# If we haven't found title yet, this might be it
if not title:
# Remove markdown headers
potential_title = line_stripped.replace('#', '').strip()
# Check if this line contains credentials (common patterns)
if any(cred in potential_title.lower() for cred in ['dr.', 'dr ', 'phd', 'ph.d', 'credentials', 'expert', 'years']):
# This might be credentials, skip it and look for title before it
if i > 0:
# Previous line might be title
prev_line = lines[i-1].strip().replace('#', '').strip()
if prev_line:
title = prev_line
article_start_idx = i + 1
credentials_found = True
break
continue
# If line ends with common credential patterns, split it
if ' - ' in potential_title or ' | ' in potential_title:
parts = potential_title.split(' - ') if ' - ' in potential_title else potential_title.split(' | ')
title = parts[0].strip()
article_start_idx = i + 1
credentials_found = True
break
else:
# This might be the title, but check next line for credentials
title = potential_title
article_start_idx = i + 1
# Check if this is a credentials line (after title)
elif not credentials_found and any(cred in line_stripped.lower() for cred in ['dr.', 'dr ', 'phd', 'ph.d', 'expert', 'years of experience']):
credentials_found = True
article_start_idx = i + 1
break
# If we've passed potential title/credentials, this is article content
elif title and not credentials_found and line_stripped:
# No credentials found, article starts here
article_start_idx = i
break
# Clean up title
if not title:
# Fallback: use first non-empty line
for line in lines:
line_stripped = line.strip().replace('#', '').strip()
if line_stripped:
title = line_stripped
break
if len(title) > 100:
title = title[:97] + "..."
# Extract article content (skip title and credentials)
article_lines = lines[article_start_idx:]
article_content = '\n'.join(article_lines).strip()
# Generate summary from first paragraph of article
summary = ""
for line in article_lines:
line = line.strip()
if line and not line.startswith('#') and len(line) > 20:
summary = line[:150]
if len(line) > 150:
summary += "..."
break
if not summary:
summary = "A new insight on sterile hydroponics and cultivation techniques."
return title, summary, article_content
def main():
"""Main function to generate and save blog post."""
# Ensure blog directory exists
BLOG_DIR.mkdir(parents=True, exist_ok=True)
# Check if Ollama is available
content = call_ollama()
if content is None:
# Fail quietly - exit with code 0 so systemd doesn't log errors
print("Ollama endpoint unavailable. Will retry in 3 days.", file=sys.stderr)
sys.exit(0)
# Get next post number
post_number = get_next_post_number()
filename = generate_filename(post_number)
# Extract title, summary, and article content (separate from credentials)
title, summary, article_content = extract_title_and_summary(content)
# Create frontmatter
frontmatter = create_frontmatter(title, summary)
# Combine frontmatter and article content (not the full content with credentials)
full_content = frontmatter + "\n" + article_content + "\n"
# Write to file
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(full_content)
print(f"Successfully created blog post: {filename}")
print(f"Title: {title}")
return 0
except Exception as e:
print(f"ERROR: Failed to write blog post: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
sys.exit(main())