I have a HaikuBox listening to bird calls near my house. This morning’s coffee tinker is collecting visit data from the API, passing it to Ollama with a custom prompt and outputting a casual PDF report I can read.
import requests
from datetime import date
import json
import re
from reportlab.lib import colors
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import inch
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
# API URLs and Configuration
HAIKUBOX_ID = "APIKEY"
API_URL = f"https://api.haikubox.com/haikubox/{HAIKUBOX_ID}/daily-count"
OLLAMA_URL = "http://192.168.1.2:7869/api/generate"
def get_bird_counts():
"""Fetch bird count data from HaikuBox API."""
today = date.today().isoformat()
response = requests.get(f"{API_URL}?date={today}")
if response.status_code == 200:
return response.json()
else:
print(f"Error fetching data: {response.status_code}")
return None
def create_analysis_prompt(bird_data):
"""Create a structured prompt for AI analysis."""
today = date.today().isoformat()
bird_list = "\n".join([f"- {bird['bird']}: {bird['count']} calls heard" for bird in bird_data])
prompt = f"""You are a passionate nature documentary narrator recounting the day's bird activities at the Burns farm on {today}. Using the bird visit data below with location and time of year, craft an engaging and entertaining narrative that's personal, casual, and fun to read. Point out interesting facts about each species and share notable observations in the style of a captivating story.
Bird Visit Data: {bird_list}
Guidelines:
Tone & Style:
Write in a friendly, short and casual manner.
Content Structure:
Highlight the birds and their behaviors, including interesting facts and anecdotes about each species.
Highlight any unusual patterns, surprises, or standout moments from the day, including geographical related information for the Pacific Northwest.
Conclude with any tips for how we should feed or help support them.
Formatting Rules:
Avoid using any markdown symbols or special formatting characters (*, -, #, etc.).
Include fun section headers and keep the narrative flowing smoothly from one section to the next.
Place a blank line between paragraphs for readability.
Keep it short and concise, this should be something that can be read quickly.
Remember:
Your goal is to create an enjoyable and informative read that immerses the reader in the day's birdwatching experience, much like a segment from a beloved nature documentary.
"""
return prompt
def get_ollama_analysis(prompt):
"""Get AI analysis from Ollama."""
headers = {"Content-Type": "application/json"}
payload = {
"model": "llama3.1",
"prompt": prompt,
"stream": False
}
try:
response = requests.post(OLLAMA_URL, headers=headers, json=payload)
if response.status_code == 200:
return response.json()["response"]
else:
print(f"Error getting analysis: {response.status_code}")
return None
except Exception as e:
print(f"Exception while getting analysis: {e}")
return None
def format_analysis_text(analysis):
"""Format the analysis text with proper structure and lists."""
formatted_sections = []
current_section = []
# Split into lines and clean up
lines = [line.strip() for line in analysis.split('\n') if line.strip()]
for line in lines:
# Clean up any markdown or special characters
line = re.sub(r'[\*\#\-]+', '', line)
# Check for section headers (capitalized words)
if re.match(r'^[A-Z][A-Za-z\s]+$', line):
if current_section:
formatted_sections.extend(current_section)
formatted_sections.append('') # Add spacing between sections
current_section = [f"HEADER:{line}"]
# Handle bullet points
elif line.startswith('•'):
current_section.append(f"BULLET:{line[1:].strip()}")
elif line.lstrip().startswith('•'):
current_section.append(f"BULLET:{line.lstrip()[1:].strip()}")
# Convert other list-like lines to bullets
elif re.match(r'^\d+\.\s+', line):
cleaned_line = re.sub(r'^\d+\.\s+', '', line)
current_section.append(f"BULLET:{cleaned_line}")
else:
current_section.append(line)
# Add the last section
if current_section:
formatted_sections.extend(current_section)
return formatted_sections
def create_pdf_styles():
"""Create and return PDF styles."""
styles = getSampleStyleSheet()
return {
'title': ParagraphStyle(
'CustomTitle',
parent=styles['Heading1'],
fontSize=24,
spaceAfter=30,
textColor=colors.HexColor('#2C3E50'),
alignment=1 # Center alignment
),
'date': ParagraphStyle(
'DateStyle',
parent=styles['Normal'],
fontSize=12,
textColor=colors.HexColor('#7F8C8D'),
alignment=1 # Center alignment
),
'section': ParagraphStyle(
'SectionHeader',
parent=styles['Heading2'],
fontSize=16,
spaceBefore=20,
spaceAfter=12,
textColor=colors.HexColor('#34495E')
),
'subsection': ParagraphStyle(
'SubsectionHeader',
parent=styles['Heading3'],
fontSize=14,
spaceBefore=16,
spaceAfter=8,
textColor=colors.HexColor('#2980B9')
),
'body': ParagraphStyle(
'CustomBody',
parent=styles['Normal'],
fontSize=11,
leading=14,
textColor=colors.HexColor('#2C3E50'),
spaceBefore=6,
spaceAfter=6
),
'bullet': ParagraphStyle(
'BulletStyle',
parent=styles['Normal'],
fontSize=11,
leading=14,
leftIndent=20,
bulletIndent=10,
spaceBefore=3,
spaceAfter=3,
textColor=colors.HexColor('#2C3E50')
),
'footer': ParagraphStyle(
'Footer',
parent=styles['Normal'],
fontSize=10,
textColor=colors.HexColor('#95A5A6'),
alignment=1 # Center alignment
)
}
def create_pdf_report(bird_data, analysis, output_filename):
"""Create a professionally formatted PDF report."""
doc = SimpleDocTemplate(
output_filename,
pagesize=letter,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72
)
styles = create_pdf_styles()
story = []
# Title and Date
story.append(Paragraph("Bird Analysis Report", styles['title']))
story.append(Paragraph(f"Date: {date.today().isoformat()}", styles['date']))
story.append(Spacer(1, 30))
# Bird Count Summary
story.append(Paragraph("Bird Count Summary", styles['section']))
story.append(Spacer(1, 12))
# Create and style the table
sorted_bird_data = sorted(bird_data, key=lambda x: x['count'], reverse=True)
table_data = [['Species', 'Visits']]
table_data.extend([[bird['bird'], str(bird['count'])] for bird in sorted_bird_data])
table = Table(table_data, colWidths=[4*inch, 1*inch])
table.setStyle(TableStyle([
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor('#34495E')),
('TEXTCOLOR', (0, 0), (-1, 0), colors.white),
('ALIGN', (0, 0), (-1, -1), 'LEFT'),
('ALIGN', (1, 0), (1, -1), 'RIGHT'),
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
('FONTSIZE', (0, 0), (-1, 0), 12),
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
('BACKGROUND', (0, 1), (-1, -1), colors.white),
('TEXTCOLOR', (0, 1), (-1, -1), colors.HexColor('#2C3E50')),
('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
('FONTSIZE', (0, 1), (-1, -1), 10),
('GRID', (0, 0), (-1, -1), 1, colors.HexColor('#BDC3C7')),
('LEFTPADDING', (0, 0), (-1, -1), 8),
('RIGHTPADDING', (0, 0), (-1, -1), 8),
('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#F9F9F9')])
]))
story.extend([table, Spacer(1, 30)])
# Analysis Section
story.append(Paragraph("Analysis", styles['section']))
story.append(Spacer(1, 12))
# Format and add analysis text
formatted_sections = format_analysis_text(analysis)
for section in formatted_sections:
if section.startswith('HEADER:'):
header_text = section.replace('HEADER:', '').strip()
story.append(Spacer(1, 12))
story.append(Paragraph(header_text, styles['subsection']))
elif section.startswith('BULLET:'):
bullet_text = section.replace('BULLET:', '').strip()
story.append(Paragraph(f"• {bullet_text}", styles['bullet']))
elif section: # Only add non-empty sections
story.append(Paragraph(section, styles['body']))
# Build the PDF
try:
doc.build(story)
print(f"PDF report created: {output_filename}")
return True
except Exception as e:
print(f"Error creating PDF: {e}")
return False
def save_analysis(bird_data, analysis):
"""Save the analysis data as JSON."""
today = date.today().isoformat()
log_entry = {
"date": today,
"bird_data": bird_data,
"analysis": analysis
}
filename = f"bird_analysis_{today}.json"
try:
with open(filename, 'w') as f:
json.dump(log_entry, f, indent=2)
print(f"Analysis saved to {filename}")
return filename
except Exception as e:
print(f"Error saving analysis: {e}")
return None
def main():
# Get bird count data
bird_data = get_bird_counts()
if not bird_data:
print("Failed to get bird count data")
return
# Get analysis
prompt = create_analysis_prompt(bird_data)
analysis = get_ollama_analysis(prompt)
if analysis:
# Print analysis
print("\nAI Analysis of Bird Data:")
print("------------------------")
print(analysis)
# Save data
save_analysis(bird_data, analysis)
# Create and open PDF report
today = date.today().isoformat()
pdf_filename = f"bird_analysis_{today}.pdf"
create_pdf_report(bird_data, analysis, pdf_filename)
# Open the PDF
try:
if os.name == 'nt': # Windows
os.startfile(pdf_filename)
else: # macOS and Linux
os.system(f'open "{pdf_filename}"')
print(f"Opened PDF report: {pdf_filename}")
except Exception as e:
print(f"Error opening PDF: {e}")
print(f"The PDF file has been created at: {pdf_filename}")
else:
print("Failed to get analysis from Ollama")
if __name__ == "__main__":
main()