WildGardenGnome
Published © GPL3+

Philosotron: Automating the Thinkers

A robot that stares out of a window, and provides cryptic messages to be misunderstood and fought over for centuries.

IntermediateFull instructions provided5 hours36
Philosotron: Automating the Thinkers

Things used in this project

Hardware components

Raspberry Pi 4 Model B
Raspberry Pi 4 Model B
×1
Flash Memory Card, MicroSD Card
Flash Memory Card, MicroSD Card
×1
Model W3 1080p Webcam
×1
Voltaic v88
Optional
×1
Voltaic Cable
Optional
×1
Adafruit PCA9685
Optional
×1
MG996r 180 Degrees
Optional
×2
Adjustable Step Down - LM2596S
Optional
×1
M-M Jumper wire
Optional
×2
F-M Jumper Wires
Optional
×6
USB Cable, USB Type C Plug
USB Cable, USB Type C Plug
Optional
×1
A Box
×1
Velcro stickers
Optional
×1

Software apps and online services

Raspbian
Raspberry Pi Raspbian
Moondream API
Google Gemini API
Raspberry Pi Imager
FileZilla
PuTTY
Microsoft Visual Studio Code
Flask
OpenCV
OpenCV
TailwindCSS

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Optional
Multitool, Screwdriver
Multitool, Screwdriver
Optional
Super Glue
Optional

Story

Read more

Schematics

Wiring

Code

ServoCentering

Python
from adafruit_servokit import ServoKit
import time
import random

# Initialize the PCA9685 and servos
kit = ServoKit(channels=16, frequency=50)

panServo = 15 
kit.servo[panServo].actuation_range = 180
kit.servo[panServo].set_pulse_width_range(500, 2500)


tiltServo = 12
kit.servo[tiltServo].actuation_range = 180
kit.servo[tiltServo].set_pulse_width_range(500, 2500)

def setToCenter():
    
    kit.servo[panServo].angle =  90
    time.sleep(1)
    kit.servo[tiltServo].angle = 90
    time.sleep(1)
    
def moveCamera():
    
    print("Moving the Camera.")
    
    kit.servo[panServo].angle =  random.randint(10, 170)
    time.sleep(1)
    kit.servo[tiltServo].angle = random.randint(70, 110)
    time.sleep(1)
    
    

# For servo centenrig
setToCenter()
time.sleep(1)
print("Done centering both servos, you ca now attach the arms.")

# # For testing the range
# try:
#     while True:
#         moveCamera()
    
# except KeyboardInterrupt:
#     print("Philosotron is powering down...")
#     kit.servo[panServo].angle =  90
#     time.sleep(1)
#     kit.servo[tiltServo].angle = 90

codeBaseClean.py

Python
import time
from datetime import datetime
import os
import cv2
import moondream as md
from PIL import Image
import json
from google import genai
from adafruit_servokit import ServoKit
import random

# Initialize the PCA9685 and servos
kit = ServoKit(channels=16, frequency=50)

panServo = 15 
kit.servo[panServo].actuation_range = 180
kit.servo[panServo].set_pulse_width_range(500, 2500)


tiltServo = 12 
kit.servo[tiltServo].actuation_range = 180
kit.servo[tiltServo].set_pulse_width_range(500, 2500)

# Gemini model setup
client = genai.Client(api_key="XXX")

# Moondream model
model = md.vl(api_key="XXX")

# Base folders
basePictureFolder = "pictures"
logFolder = "logs" 

# Ensure main folders exist
for d in [basePictureFolder, logFolder]:
    if not os.path.exists(d):
        os.makedirs(d)

cooldownSeconds = 120

def moveCamera():
    
    print("Moving the Camera.")
    
    kit.servo[panServo].angle =  random.randint(10, 170)
    time.sleep(1)
    kit.servo[tiltServo].angle = random.randint(70, 110)
    time.sleep(1)
    
    

def getLatestThought():
    
    currentDate = datetime.now().strftime("%Y-%m-%d")
    filename = os.path.join(logFolder, f"history_{currentDate}.json")
    
    if os.path.exists(filename):
        
        with open(filename, 'r') as f:
            
            try:
                
                data = json.load(f)
                
                if data:
                    return data[-1].get("thought", "")
                
            except:
                pass
            
    return "I have just opened my eyes."

def saveToDatedJSON(data):
    
    currentDate = datetime.now().strftime("%Y-%m-%d")
    filename = os.path.join(logFolder, f"history_{currentDate}.json")
    
    listToSave = []
    
    if os.path.exists(filename):
        with open(filename, 'r') as f:
            try:
                listToSave = json.load(f)
            except:
                listToSave = []

    listToSave.append(data)
    
    with open(filename, 'w') as f:
        json.dump(listToSave, f, indent=4)

def getAISummary(frame):
    
    frameRgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    pilImg = Image.fromarray(frameRgb)
    
    try:
        shortResult = model.caption(pilImg, length="short")
        return shortResult["caption"]
    except Exception as e:
        return f"Vision Error: {e}"

def getAIThought(imageSummary, previousThought):
    
    prompt = f"""
        Internal Monologue History: '{previousThought}'
        Current Sensory Report: '{imageSummary}'.

        As the Philosotron, you must ignore the mundane. 
        Translate this visual data into a philosophical platitude that builds upon your previous thought. 
        Do not repeat yourself. Sound like a guru who has seen too much. Be concise.
        """
    try:
        response = client.models.generate_content(model="gemini-2.5-flash-lite", contents=prompt)
        return response.text.strip()
    except Exception as e:
        return f"Crisis: {e}"

print("Awakening")

video = cv2.VideoCapture(0)

if not video.isOpened():
    print("Error: Could not open webcam.")
    exit()

# --- Main Loop ---
try:
    
    while True:
        
        # Prepare Daily Folder
        currentDate = datetime.now().strftime("%Y-%m-%d")
        dayFolder = os.path.join(basePictureFolder, currentDate)
        os.makedirs(dayFolder, exist_ok=True)

        moveCamera()
        time.sleep(2) 
        
        for _ in range(60):
            video.read()
        
        check, frame = video.read()

        if check:
            
            # Get the timestamp
            now = datetime.now()
            timestampStr = now.strftime("%H-%M-%S")

            # Get the caption, last thought and new insight
            aiSummary = getAISummary(frame)
            lastThought = getLatestThought()
            aiThought = getAIThought(aiSummary, lastThought)

            # Save Image in the designated folder
            imageFilename = f"image_{timestampStr}.jpg"
            filepath = os.path.join(dayFolder, imageFilename)
            cv2.imwrite(filepath, frame)

            # Log data
            entry = {
                "time": timestampStr,
                "folder": currentDate,
                "image": imageFilename,
                "summary": aiSummary,
                "thought": aiThought
            }
            
            # Save the data in a local json
            saveToDatedJSON(entry)
            
            print(f"Captured: {imageFilename} -> {aiSummary} -> {aiThought}")
        
        # Go to sleep
        time.sleep(cooldownSeconds)
        
except KeyboardInterrupt:
    
    print("Philosotron is powering down...")
    
finally:
    
    video.release()
    kit.servo[panServo].angle =  90
    time.sleep(1)
    kit.servo[tiltServo].angle = 90
    

backend.py

Python
import os
from flask import Flask, render_template, jsonify, send_from_directory
from flask_cors import CORS

app = Flask(__name__)
CORS(app)

LOGS_DIR = os.path.join(os.getcwd(), 'logs')
PICTURES_DIR = os.path.join(os.getcwd(), 'pictures')

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/dates')
def get_dates():
    dates = []
    if os.path.exists(LOGS_DIR):
        for filename in os.listdir(LOGS_DIR):
            if filename.startswith("history_") and filename.endswith(".json"):
                # Extracts "2026-04-04" from "history_2026-04-04.json"
                date_str = filename.replace("history_", "").replace(".json", "")
                dates.append(date_str)
    
    # Sort strings like "2026-04-04" alphabetically in reverse 
    # results in the latest date being at the top (index 0).
    return jsonify(sorted(dates, reverse=True))

@app.route('/api/logs/<date>')
def get_log(date):
    return send_from_directory(LOGS_DIR, f"history_{date}.json")

@app.route('/pictures/<path:filename>')
def serve_pictures(filename):
    return send_from_directory(PICTURES_DIR, filename)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5500, debug=True)

index.html

HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Philosotron</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&family=Lora:ital,wght@1,400&display=swap');
        body { font-family: 'Inter', sans-serif; background-color: #fafafa; }
        .thought-text { font-family: 'Lora', serif; }
    </style>
</head>
<body class="text-slate-800">

    <div class="max-w-5xl mx-auto px-4 py-12">
        <header class="mb-16 border-b border-slate-200 pb-8 flex flex-col md:flex-row justify-between items-start md:items-end gap-4">
            <div>
                <h1 class="text-3xl font-light tracking-tight text-slate-900 italic">Philosotron</h1>
                <p class="text-slate-500 mt-2">A daily record of observations and reflections.</p>
            </div>
            
            <div class="flex flex-col items-end w-full md:w-auto">
                <label for="dateSelect" class="text-xs uppercase tracking-widest text-slate-400 mb-2 font-bold">Select Archive Date</label>
                <select id="dateSelect" class="w-full md:w-64 bg-white border border-slate-200 rounded-lg px-4 py-2 outline-none focus:ring-2 focus:ring-indigo-100 transition-all cursor-pointer shadow-sm">
                    </select>
            </div>
        </header>

        <main id="gallery-feed" class="space-y-32">
            <div class="text-center py-20 text-slate-400">Initializing archive...</div>
        </main>
    </div>

    <script>
        const feedContainer = document.getElementById('gallery-feed');
        const dateSelect = document.getElementById('dateSelect');
    
        // Utility to turn "12-43-08" into "12:43"
        function formatTime(rawTime) {
            if (!rawTime) return "";
            // Replaces all dashes with colons
            return rawTime.replace(/-/g, ':');
        }
    
        async function init() {
            try {
                const response = await fetch('/api/dates');
                const logDates = await response.json();
    
                if (!logDates || logDates.length === 0) {
                    feedContainer.innerHTML = '<p class="text-center py-20">No logs found.</p>';
                    return;
                }
    
                // Populate dropdown (already sorted by Python)
                dateSelect.innerHTML = logDates.map(date => 
                    `<option value="${date}">${date}</option>`
                ).join('');
                
                // Load the latest date
                loadData(logDates[0]);
            } catch (err) {
                console.error("API Error:", err);
            }
        }
    
        async function loadData(date) {
            feedContainer.innerHTML = '<div class="text-center py-20 text-slate-400 animate-pulse">Loading Archive...</div>';
            try {
                const response = await fetch(`/api/logs/${date}`);
                const data = await response.json();
                renderFeed(data);
            } catch (err) {
                feedContainer.innerHTML = '<p class="text-center py-20 text-red-500">Error loading log.</p>';
            }
        }
    
        function renderFeed(items) {
            // Sort individual entries within the day by time (latest entry at top)
            const sortedItems = items.sort((a, b) => b.time.localeCompare(a.time));
    
            feedContainer.innerHTML = sortedItems.map(item => `
                <article class="grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-16 items-start mb-24 border-b border-slate-100 pb-16 last:border-0">
                    <div class="lg:col-span-7">
                        <div class="rounded-2xl overflow-hidden shadow-lg bg-white">
                            <img src="/pictures/${item.folder}/${item.image}" class="w-full h-auto">
                        </div>
                    </div>
    
                    <div class="lg:col-span-5 pt-4">
                        <div class="flex items-center gap-3 mb-4">
                            <span class="bg-slate-900 text-white text-xs font-bold px-2 py-1 rounded">
                                ${formatTime(item.time)}
                            </span>
                            <span class="text-slate-300 text-xs uppercase tracking-widest">${item.folder}</span>
                        </div>
                        
                        <p class="text-slate-600 text-lg mb-8 leading-relaxed">
                            ${item.summary}
                        </p>
                        
                        <div class="relative pl-6 border-l-2 border-indigo-100">
                            <p class="thought-text text-2xl text-slate-400 italic leading-tight">
                                ${item.thought}
                            </p>
                        </div>
                    </div>
                </article>
            `).join('');
        }
    
        dateSelect.addEventListener('change', (e) => loadData(e.target.value));
        init();
    </script>
</body>
</html>

Credits

WildGardenGnome
1 project • 0 followers
A wild garden gnome in a high-tech world.

Comments