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.
321 lines
11 KiB
Python
Executable File
321 lines
11 KiB
Python
Executable File
#!/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())
|
|
|