#!/bin/bash # mac-narrator -- your Mac's inner monologue, powered by Apple Intelligence # Install globally as `apfel-mac-narrator`: see demo/README.md "Install demos globally (optional)" # # One-shot: mac-narrator # Watch: mac-narrator --watch # mac-narrator --watch --interval 30 # Speak: mac-narrator --say # Quiet: mac-narrator --watch --threshold 50 (only narrate when score > 50) # # Requires: apfel installed (https://github.com/Arthur-Ficial/apfel) INTERVAL=60 WATCH=false SAY=false THRESHOLD=0 # 0 = always narrate, 1-100 = only when interesting enough while [[ $# -gt 0 ]]; do case "$1" in --watch|-w) WATCH=true; shift ;; --interval|-i) INTERVAL="$2"; shift 2 ;; --say|-s) SAY=true; shift ;; --threshold|-t) THRESHOLD="$2"; shift 2 ;; -h|--help) echo "mac-narrator -- your Mac's inner monologue" echo "" echo "Usage: mac-narrator [OPTIONS]" echo "" echo " --watch, -w Continuous mode (default: every 60s)" echo " --interval N, -i N Seconds between narrations" echo " --say, -s Speak narration aloud (macOS say)" echo " --threshold N, -t N Only narrate when interest score > N (0-100)" echo " Score based on: CPU, memory pressure, battery, disk" echo " 0 = always narrate (default)" echo " 30 = only mildly interesting (a process > 30% CPU)" echo " 70 = only dramatic (battery < 20%, memory full, etc.)" echo "" echo "Requires: apfel (Apple Intelligence CLI)" exit 0 ;; *) echo "Unknown option: $1. Use --help."; exit 1 ;; esac done if ! command -v apfel &>/dev/null; then echo "Error: apfel not found. Install from https://github.com/Arthur-Ficial/apfel" exit 1 fi PROMPT="You narrate this computer's life like a nature documentary. Given system data, respond with EXACTLY 1-2 short sentences. Be specific about process names and numbers. Dry British humor. No bullet points, no lists." # Compute an interest score (0-100) from system metrics. # Higher = more interesting = more worth narrating. compute_score() { local score=0 # Top CPU process (> 50% = interesting, > 90% = dramatic) local top_cpu top_cpu=$(ps -eo %cpu -r 2>/dev/null | head -2 | tail -1 | tr -d ' ') top_cpu=${top_cpu%.*} # truncate to int if [ "${top_cpu:-0}" -gt 90 ] 2>/dev/null; then score=$((score + 40)) elif [ "${top_cpu:-0}" -gt 50 ] 2>/dev/null; then score=$((score + 25)) elif [ "${top_cpu:-0}" -gt 30 ] 2>/dev/null; then score=$((score + 15)) fi # Memory pressure (free < 30% = interesting, < 10% = dramatic) local mem_free mem_free=$(memory_pressure 2>/dev/null | grep "System-wide" | grep -o '[0-9]*' | head -1) if [ "${mem_free:-100}" -lt 10 ] 2>/dev/null; then score=$((score + 40)) elif [ "${mem_free:-100}" -lt 30 ] 2>/dev/null; then score=$((score + 20)) elif [ "${mem_free:-100}" -lt 50 ] 2>/dev/null; then score=$((score + 10)) fi # Battery (< 20% = interesting, < 5% = dramatic, charging = boring) local batt_pct batt_pct=$(pmset -g batt 2>/dev/null | grep -o '[0-9]*%' | tr -d '%') local batt_state batt_state=$(pmset -g batt 2>/dev/null | tail -1) if [ -n "$batt_pct" ] && ! echo "$batt_state" | grep -q "charged\|charging"; then if [ "$batt_pct" -lt 5 ] 2>/dev/null; then score=$((score + 30)) elif [ "$batt_pct" -lt 20 ] 2>/dev/null; then score=$((score + 15)) fi fi # Disk (> 80% full = interesting, > 95% = dramatic) local disk_pct disk_pct=$(df -h / 2>/dev/null | tail -1 | awk '{print $5}' | tr -d '%') if [ "${disk_pct:-0}" -gt 95 ] 2>/dev/null; then score=$((score + 30)) elif [ "${disk_pct:-0}" -gt 80 ] 2>/dev/null; then score=$((score + 15)) fi # Load average (> 4 = interesting for most Macs) local load load=$(sysctl -n vm.loadavg 2>/dev/null | awk '{print $2}' | cut -d. -f1) if [ "${load:-0}" -gt 8 ] 2>/dev/null; then score=$((score + 20)) elif [ "${load:-0}" -gt 4 ] 2>/dev/null; then score=$((score + 10)) fi # Cap at 100 if [ "$score" -gt 100 ]; then score=100; fi echo "$score" } narrate() { # Check threshold if [ "$THRESHOLD" -gt 0 ]; then local score score=$(compute_score) if [ "$score" -lt "$THRESHOLD" ]; then if $WATCH; then echo -e "\033[90m[$(date +%H:%M:%S)] score $score/$THRESHOLD -- quiet\033[0m" fi return fi if $WATCH; then echo -e "\033[90m[$(date +%H:%M:%S)] score $score/$THRESHOLD -- narrating\033[0m" fi fi local snapshot snapshot=$( ps -eo pid,%cpu,%mem,comm -r 2>/dev/null | head -8 echo "---" memory_pressure 2>/dev/null | head -3 echo "---" df -h / 2>/dev/null | tail -1 echo "---" pmset -g batt 2>/dev/null | tail -1 echo "---" uptime 2>/dev/null ) local oneline oneline=$(echo "$snapshot" | tr '\n' '; ') local comment if comment=$(apfel -q --max-tokens 150 -s "$PROMPT" "System snapshot: $oneline"); then echo -e "\033[90m[$(date +%H:%M:%S)]\033[0m $comment" if $SAY; then say "$comment" fi else echo -e "\033[90m[$(date +%H:%M:%S)]\033[0m \033[33m(model busy, skipping)\033[0m" fi } if $WATCH; then echo -e "\033[36mapfel mac-narrator\033[0m watching every ${INTERVAL}s (Ctrl+C to stop)" if [ "$THRESHOLD" -gt 0 ]; then echo -e "\033[90mthreshold: $THRESHOLD (only narrating when interesting enough)\033[0m" fi if $SAY; then echo -e "\033[90mspeech: on\033[0m" fi echo "" while true; do narrate echo "" sleep "$INTERVAL" done else narrate fi