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:
320
scripts/generate_blog_post.py
Executable file
320
scripts/generate_blog_post.py
Executable 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())
|
||||
|
||||
Reference in New Issue
Block a user