#!/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())