A computer-vision sentry that watches a bird nest, identifies predators in real time, and plays distress calls through a speaker to scare them off. Multi-target: crows during the day, cats at night, others wired in via config.
Built after two consecutive crow attacks wiped out the eggs of a small bird family nesting on our front porch. The third clutch isn't going down without a fight.
How it works
A three-stage detection pipeline, cheapest checks first. Each stage is a filter; only frames that pass make it to the next. The Claude refinement step is per-target and optional — broad YOLO classes (like "bird" → really a crow?) use it, specific ones (like "cat") skip it to save cost.
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ Webcam │──▶│ 1. Motion │──▶│ 2. YOLOv8n │──▶│ 3. Claude (opt) │
│ every 1s │ │ (cv2 absdiff)│ │ "bird? │ │ "is the bird │
│ │ │ ~5ms, free │ │ cat? │ │ a crow?" │
│ │ │ │ │ dog? ..." │ │ ~600ms, paid │
│ │ │ │ │ ~140ms, free │ │ skipped for cat │
└──────────────┘ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘
│ no │ no │ yes / skipped
▼ ▼ ▼
skip skip 🚨 Play target-specific
deterrent through
Bluetooth speaker
Rising-edge triggering
The speaker fires when a target first appears in the frame, not continuously while it's there:
- Crow lands → 🚨 SPEAKER FIRED
- Crow stays in frame → silence (Claude API is not called — saves cost)
- Crow leaves (5+ empty YOLO frames in a row) → state resets
- Crow returns → 🚨 SPEAKER FIRED again
Stubborn-crow insurance: if a crow refuses to leave and stays in frame longer than PERSISTENT_REFIRE_SECONDS (default: 3 minutes), the speaker fires again.
Design principle: fail toward more alarms, not fewer
A false positive plays an extra speaker sound. A false negative means dead eggs. So every stage in the pipeline escalates on uncertainty:
- Motion borderline → run YOLO anyway every 30th frame (catches silent landings)
- YOLO confidence low (0.25 threshold) → still escalate to Claude
- Claude API errors, network out, or unclear response → fire the speaker anyway
Habituated-crow escalation
Crows learn fast. After ~10–20 exposures to the same stimulus, they figure out the speaker is harmless and ignore it. crowbuster detects this and escalates:
- First detection — random distress sound (rising-edge fire)
- 3.5 minutes later, crow still there — different distress sound (persistent-refire #1)
- 7 minutes in, still there —
🆘 HUMAN ALARMplayssounds/alarm.wav— a sound you will recognize and respond to by physically going outside
Add sounds/alarm.wav to enable. The script falls back to a regular distress sound if it's missing. The counter resets the moment the crow leaves the frame, so a determined-but-mobile crow won't trigger the alarm; only a truly habituated one that refuses to budge does.
Safeguards that work even if detection is broken
- Heartbeat file — script writes
./heartbeatevery 60s. If the file goes stale, you know the script died. - Auto camera reopen — if the camera read fails mid-loop, the script reopens it.
@rebootcron — script auto-restarts on boot.- Captured frames — every triggering frame is saved to
./captures/with a timestamp, so you can review what tripped the system and retune. - Shuffle-bag audio rotation — cycles through every mp3 in
./sounds/before any repeats, then reshuffles. Defeats crow habituation faster than pure random.
What you'll see in the logs
[15:20:01] crowbuster started [production] (model=claude-haiku-4-5, loop=1s)
[15:20:03] ⏵ motion started (diff=12.4)
[15:20:03] → YOLO: bird FOUND (conf=0.87, 142ms)
[15:20:04] → Claude: YES (612ms)
[15:20:04] 🚨 SPEAKER FIRED (crow) — playing 116-Crow & Hawk Fight.mp3
[15:20:08] → YOLO: bird FOUND (conf=0.91, 138ms)
[15:20:08] → crow still in frame — not re-firing
[15:20:35] ⏸ motion stopped (diff=2.1)
[15:20:45] ⏸ crow left the frame (after 5 empty YOLO checks)
[15:25:00] stats: frames=300 motion=42 yolo=18 birds=4 crows=2
Every 5 minutes a stats line summarizes pipeline activity, so you can verify the script is alive even when there's nothing to report.
Hardware
- A spare laptop with a webcam (this runs on a 2012-era ThinkPad)
- A Bluetooth speaker placed near the nest
- Wi-Fi
- No Raspberry Pi, soldering, or enclosure required
Setup
1. Install system dependencies (Ubuntu / Debian)
sudo apt update
sudo apt install -y python3-pip python3-venv python3-opencv \
ffmpeg libsdl2-mixer-2.0-02. Install Python dependencies
git clone https://github.com/<your-fork>/crowbuster.git cd crowbuster python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt
Heads up:
ultralyticspulls in PyTorch, which is a ~500MB download. First run also auto-downloads the YOLOv8n model (~6MB). On a CPU-only old laptop, you can reclaim ~3GB by reinstalling torch as CPU-only:pip uninstall -y torch torchvision nvidia-* pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
3. Sound files
A few crow distress mp3s ship in ./sounds/. Add more for better variety — the shuffle-bag picks them up automatically. Good sources:
- HME Products — Free Predator Calls
- xeno-canto.org — search "Corvus brachyrhynchos alarm" (Creative Commons licensed)
Mix in hawk and owl calls — crows fear those too, and variety defeats habituation. See sounds/SOURCES.txt for attribution.
Audio length: Aim for 5–15 second clips. Crows are gone within seconds of the first burst; long sustained sounds just block detection and train crows to ignore the noise. The script caps playback at MAX_PLAY_SECONDS (default 15s) so longer files won't stall the pipeline, but trimming them is cleaner.
4. Set your Anthropic API key
Grab one at console.anthropic.com → API Keys, then:
cp .env.example .env
# edit .env and set ANTHROPIC_API_KEY=sk-ant-...crowbuster.py auto-loads .env via python-dotenv. .env is gitignored, so your key stays out of the repo. If you'd rather use a shell env var, that works too — os.environ takes precedence.
5. Pair the Bluetooth speaker
bluetoothctl power on scan on # find the speaker MAC in the list pair XX:XX:XX:XX:XX:XX connect XX:XX:XX:XX:XX:XX trust XX:XX:XX:XX:XX:XX exit
Set it as default audio output in your sound settings.
6. (Optional) Phone alerts via ntfy.sh
Push a notification to your phone with the triggering frame attached every time the system catches a real predator. Two priorities:
- Default priority — fired on every confirmed new detection (rising-edge). Your phone settings decide whether it makes sound.
- Urgent priority — fired on the
🆘 HUMAN ALARMescalation (target refuses to leave after multiple refires). Loud, breaks through Do Not Disturb on most phones.
Persistent-refires of the same target intentionally do NOT re-alert — once you know a crow is on the porch, you don't need 4 more buzzes over the next 10 minutes. New animal = new ping.
Health-check pings ride on the same channel so you know the system is alive even when no predators have visited:
| Signal | Priority | Tag | When |
|---|---|---|---|
🟢 crowbuster started |
low | 🟢 | Process startup (after reboot, systemd restart, manual launch) |
🟢 crowbuster operational |
low | ❤️ | Every CROWBUSTER_HEARTBEAT_HOURS (default 12h) with uptime + per-target stats |
🟡 crowbuster stopped |
default | 🟡 | Graceful shutdown (Ctrl+C, SIGTERM) or fatal crash |
🚨 CAMERA DEAD |
urgent | After 5 consecutive cv2.VideoCapture.read() failures — script alive but blind |
The heartbeat pings are designed to be quiet — you won't notice them. The signal is the absence of them. If you don't see a 🟢 operational ping for >24h, the laptop's Wi-Fi died, sshd is gone, or the process crashed. The script can't alert you when the box is fully offline; this is the only reliable indicator of that case.
-
Pick a hard-to-guess topic name (e.g.
crowbuster-utkarsh-7g3pq). Anyone who knows the topic can read your alerts, so don't pick something obvious. -
Install the ntfy app — iOS, Android, or just open
ntfy.sh/<your-topic>in a browser. -
Subscribe to your topic in the app.
-
Set the env var in
.env:CROWBUSTER_NTFY_TOPIC=crowbuster-utkarsh-7g3pq
-
Restart. The startup banner will show
phone ntfy/<topic> (priority=high). If you ever want to silence it, just unset the var — no code change.
Self-hosting ntfy? Set CROWBUSTER_NTFY_SERVER=https://ntfy.yourdomain.com too. Free public ntfy.sh is fine for personal use.
7. Test the pipeline end-to-end
Before pointing it at a real nest, verify each stage works using test mode — it swaps the target from "crow" to "human" so you can stand in front of the camera:
CROWBUSTER_TEST=1 python3 crowbuster.py
You'll see crowbuster started [TEST] in the startup log along with a targets: block listing the swapped-in person target. Step in/out of frame and confirm:
| Stage | What you'll see | Proves |
|---|---|---|
| Motion | ⏵ motion started (diff=12.4) |
Camera + frame diff working |
| YOLO | → YOLO: person FOUND (human, conf=0.87, 142ms) |
Local model loaded, person class detected |
| Claude | → Claude(human): YES (612ms) |
API key + network working |
| Speaker | 🚨 SPEAKER FIRED (human, rising-edge) |
Bluetooth speaker + audio path working |
Walk out of frame, wait 5+ seconds, walk back in — speaker should fire again (rising-edge). If it fires while you're stationary, that's the persistent-refire safety kicking in after 3 minutes.
8. Run for real
Logs go to stdout and events.log. Triggering frames go to captures/. Ctrl+C to stop.
Configuration
Tweak the constants at the top of crowbuster.py:
| Setting | Default | Notes |
|---|---|---|
LOOP_INTERVAL |
1 |
Seconds between motion checks. Lower = faster reaction, more CPU. |
TARGET_GONE_AFTER_N_EMPTY |
5 |
Consecutive YOLO misses before considering the target gone (resets rising-edge). |
MAX_PLAY_SECONDS |
45 |
Truncate long audio files; keeps detection loop responsive. |
MAX_CAPTURES |
500 |
Cap on captures/ folder size (~25–50 MB). Oldest pruned first. |
CAPTURE_PRUNE_EVERY |
20 |
Check folder size every Nth save (avoids per-save filesystem stat). |
MOTION_THRESHOLD |
3.5 |
Mean blurred abs-diff cutoff. Lower = more sensitive. Tuned down from 8.0 after a 2-day prod log showed 0 birds detected — small/distant birds barely shift the mean. If empty-porch frames start triggering YOLO too often, raise toward 5–6; if you still miss landings, drop toward 2.5. |
YOLO_FORCE_CHECK_EVERY |
30 |
Run YOLO every Nth iteration even without motion (catches silent landings). |
STATS_INTERVAL_SECONDS |
300 |
How often to log pipeline activity summary. |
HEARTBEAT_SECONDS |
60 |
How often to update ./heartbeat. |
MODEL |
claude-haiku-4-5 |
Upgrade to claude-opus-4-7 if accuracy is poor. |
CAMERA_INDEX |
0 |
Built-in webcam. 1, 2, ... for USB cameras. |
DAYLIGHT_START / _END |
5:30 / 20:30 |
Window used by targets with active_hours="daylight". Targets with active_hours="always" ignore this. |
TEST_MODE |
env var | Set CROWBUSTER_TEST=1 to swap TARGETS to a single person entry for end-to-end testing. The phone alert fires in test mode too, so you can verify it end-to-end. |
CONTROL_SCREEN |
env var | Set CROWBUSTER_NO_SCREEN_CONTROL=1 to disable. By default, the script turns the display off at startup, disables the screensaver, and re-asserts the off state every 30s in a background thread (so the screensaver can't wake it). On exit (Ctrl+C, SIGTERM, or crash) the screensaver + DPMS are restored and the screen turned back on. When the script isn't running, the laptop behaves normally. |
CROWBUSTER_NTFY_TOPIC |
env var | Unset = no phone alerts. Set to a hard-to-guess topic name to push a notification (with image) on every confirmed detection. Default priority for new arrivals; urgent priority on HUMAN ALARM. See step 6. |
CROWBUSTER_NTFY_SERVER |
https://ntfy.sh |
Override only if self-hosting ntfy. |
Per-target settings (min_confidence, persistent_refire_seconds, habituation_threshold, use_claude, active_hours, etc.) live inside the TARGETS dict at the top of crowbuster.py — see Targets below.
Test-mode timing override: when
CROWBUSTER_TEST=1, theTARGETSdict is replaced with a singlepersonentry whosepersistent_refire_secondsis10andTARGET_GONE_AFTER_N_EMPTYdrops to3. The full pipeline (rising-edge → persistent-refire → habituated alarm) becomes reachable in ~30s of standing in frame instead of ~7 minutes. Production timings are unchanged.
Targets
crowbuster watches for any number of predator classes in parallel. Each entry in the TARGETS dict at the top of crowbuster.py defines one:
TARGETS = { "crow": { "yolo_class": "bird", # COCO class YOLO looks for "label": "crow", # appears in logs / capture filenames "min_confidence": 0.25, # YOLO threshold (0.0–1.0) "sounds_dir": HERE / "sounds", # folder of *.mp3 deterrents "use_claude": True, # refine YOLO → "is it really a crow?" "claude_prompt": "Is there a crow…", "active_hours": "daylight", # "daylight" or "always" "persistent_refire_seconds": 210, "habituation_threshold": 2, }, "cat": { "yolo_class": "cat", "label": "cat", "min_confidence": 0.25, "sounds_dir": HERE / "sounds" / "cat", "use_claude": False, # YOLO "cat" is precise enough — saves API cost "active_hours": "always", # cats are mostly nocturnal ... }, }
YOLO runs once per frame and routes each detection to the right state machine — adding targets doesn't multiply CPU cost. Each target keeps its own presence/refire/cooldown bookkeeping in a TargetState instance.
Why some targets skip Claude
use_claude: True is for cases where YOLO's class is broader than the actual predator. YOLO "bird" includes the resident parents on the nest, so a second-stage Claude call asks the narrower question "is this a crow?" before firing.
YOLO "cat" or "dog" are already specific. A cat on your porch is the threat — no disambiguation needed. Set use_claude: False and skip the API call entirely. Adds zero per-detection cost.
Heads up: the cat path uses a fundamentally different deterrence strategy than the bird path — crow distress audio doesn't work on cats (it attracts them as a prey signal), so the cat target relies on ultrasonic tones + phone notifications instead. The full reasoning, research, and design implications are in docs/cat-deterrence.md. Read it before adding any new mammal targets.
Adding a new target
- Find the COCO class in
yolo.names(e.g.dog=16,bear=21). Common predators: cat, dog, bear, raccoon (not in COCO — would need Claude-only). - Make
sounds/<key>/and drop deterrent mp3s in. - Add a
TARGETSentry. Restart the service.
Cost
With rising-edge triggering, Claude is only called on new appearances, so API costs are minimal in normal operation.
| Stage | Cost |
|---|---|
| Motion + YOLO (when no bird present) | $0 |
| YOLO triggers + Claude (per bird visit) | ~$0.002 |
| Typical daily bird visits | ~10–50 |
| Estimated total | ~$1–3 / month |
If even that's too much, raise LOOP_INTERVAL (slower scan) or remove the Claude stage entirely — but then YOLO will fire the speaker on any bird, scaring the nesting birds you're trying to protect. Don't do that.
Development
Auto-sync from a dev machine to the run host
Edit on your laptop, push to the run host (a separate Linux box like an old ThinkPad) automatically:
cp .env.example .env # then edit .env with your remote host details brew install fswatch # macOS ./sync.sh
Every file change triggers an rsync --delete to the remote host within a second. sync.sh reads REMOTE_USER, REMOTE_HOST, and REMOTE_PATH from .env (or from your shell env).
Run forever (auto-restart on crash, start on boot)
The repo ships a systemd user service that:
- Starts crowbuster at boot
- Restarts automatically on any crash (up to 50 times per 10 minutes)
- Keeps running when you log out (via
loginctl enable-linger) - Captures stdout + stderr in the journal
One-shot install on the run host:
That copies crowbuster.service to ~/.config/systemd/user/, enables it, starts it, and enables user lingering so the service survives logouts.
Day-to-day operations:
systemctl --user status crowbuster # current state journalctl --user -u crowbuster -f # tail the live log systemctl --user restart crowbuster # restart after editing the script systemctl --user stop crowbuster # pause it systemctl --user disable --now crowbuster # uninstall the service
The service runs from the .venv so you don't need to activate it manually. Edit crowbuster.service if you want to change the restart policy, add environment variables, or run from a different path.
Monitor the running service from a dev machine
Once the service is installed, you rarely need to be physically at the run host. SSH from your dev machine for everything.
Single-shot health check — is it running and producing fresh heartbeats?
ssh <user>@<run-host> 'systemctl --user is-active crowbuster && cat ~/crowbuster/heartbeat'
Expected output: active followed by a timestamp from the last 60 seconds. If is-active returns something other than active, or the heartbeat is more than ~2 minutes stale, the service is in trouble.
Tail the live log:
ssh <user>@<run-host> 'journalctl --user -u crowbuster -f'
Recent activity / stats:
ssh <user>@<run-host> 'journalctl --user -u crowbuster --since "1 hour ago" | grep -E "FIRED|stats|ALARM"'
Quick capture review — copy the most recent triggered frames back to your dev machine:
ssh <user>@<run-host> 'ls -t ~/crowbuster/captures/ | head -5' \ | xargs -I{} scp <user>@<run-host>:~/crowbuster/captures/{} ~/Desktop/
Recommended aliases — drop these into your dev machine's ~/.zshrc (or ~/.bashrc):
# Reads REMOTE_USER and REMOTE_HOST from your environment so the same # aliases work for any run host. Set them once in your shell rc: export REMOTE_USER=your-username export REMOTE_HOST=192.168.1.x alias cb-status='ssh $REMOTE_USER@$REMOTE_HOST "systemctl --user is-active crowbuster && cat ~/crowbuster/heartbeat"' alias cb-logs='ssh $REMOTE_USER@$REMOTE_HOST "journalctl --user -u crowbuster -f"' alias cb-restart='ssh $REMOTE_USER@$REMOTE_HOST "systemctl --user restart crowbuster"' alias cb-fires='ssh $REMOTE_USER@$REMOTE_HOST "journalctl --user -u crowbuster --since today | grep FIRED"' alias cb-screen-on='ssh $REMOTE_USER@$REMOTE_HOST "DISPLAY=:0 xset dpms force on"'
After source ~/.zshrc, you can run cb-status, cb-logs, cb-fires, etc. from anywhere on your dev machine.
Reviewing captures
captures/ fills up with timestamped jpgs labeled crow (Claude confirmed) or bird_not_crow (Claude said no). Scroll through periodically:
ls -lt captures/ | head -20If you see crows tagged bird_not_crow, the Claude prompt or model needs tuning. If you see lots of empty-frame triggers, raise MOTION_THRESHOLD. If you see crows you missed entirely, lower it.
The folder caps itself at MAX_CAPTURES (default 500) — oldest files are pruned automatically. You'll see pruned N old captures in the log when this happens.
Browse captures from a dev machine in a web browser — to skim recent frames visually without copying anything locally, run a one-line HTTP server on the run host:
ssh <user>@<run-host> cd ~/crowbuster/captures python3 -m http.server 8000
Then open http://<run-host>:8000 in any browser on the same network. Click a jpg to view inline; refresh to pick up newly written frames. Ctrl+C the SSH session when done.
Only run this on a trusted LAN —
http.serverhas no authentication.
Performance on old hardware
On a 2012 ThinkPad (4-core Intel, 4GB RAM, no GPU):
- YOLO inference: ~140ms per frame
- Claude API round-trip: ~600ms
- Idle CPU: ~5–10%
- Sustained RAM: ~800MB
Plenty of headroom for the box to keep doing other things. To squeeze more performance: switch to multi-user boot (sudo systemctl set-default multi-user.target) and skip the GUI entirely.
Aiming the camera at the nest
Use VLC (or any video capture tool) to view the webcam feed while you tilt the laptop into position. The camera can only be open in one process at a time, so stop the service first:
systemctl --user stop crowbuster # release the camera vlc v4l2:///dev/video0 # or open VLC and pick Media → Open Capture Device # aim the laptop while watching the feed, then close VLC systemctl --user start crowbuster # bring crowbuster back up
Tips for framing:
- Place the nest near one of the rule-of-thirds intersections rather than dead-center — Claude classifies better when the bird is in context
- Keep the nest in the upper third so most of the frame is empty porch, which keeps the motion baseline calm
- Aim slightly above the nest — crows approach from above, so you want to see the landing
Troubleshooting
🆘 The screen is stuck off and I can't get it back
This is the most common worry. Recovery options, easiest first:
- Press any key on the laptop's physical keyboard. Same as waking from a screensaver. Always works as long as X is alive.
- Remote panic button from another machine on the network:
Aliasing this on your dev machine is recommended:
ssh utkarsh@192.168.5.33 'DISPLAY=:0 xset dpms force on'# Add to ~/.zshrc: alias eva-screen-on='ssh utkarsh@192.168.5.33 "DISPLAY=:0 xset dpms force on"'
- Ctrl+C the running script. The
finallyblock firesxset dpms force onautomatically. - Kill the script over SSH — same effect:
- Reboot. DPMS state doesn't persist across reboots.
To never have the script touch the display:
# Add to .env:
CROWBUSTER_NO_SCREEN_CONTROL=1xset dpms force off is a runtime X server state — exactly what a screensaver does. It is not persisted, not a system config change, and survives no power cycle. You are never trapped.
No sound plays when a target is detected
-
Verify mp3s exist:
ls -la ~/crowbuster/sounds/*.mp3
The shuffle-bag needs at least one. If empty, drop some in.
-
Confirm audio is routed to the Bluetooth speaker (not the laptop speaker):
paplay ~/crowbuster/sounds/<any-file>.mp3
Should come from the BT speaker. If not, set it as default output:
pactl set-default-sink <bluez_sink_name> pactl list short sinks # find the right name
-
Check the BT connection is alive:
bluetoothctl info <speaker-mac>
-
The mpg123
id3.c:process_commenterror is harmless — the audio plays even when it appears. Strip metadata to silence the warning:sudo apt install -y eyed3 eyeD3 --remove-all sounds/*.mp3
Camera not opening / FATAL: cannot open camera
-
Test outside the script:
python3 -c "import cv2; c=cv2.VideoCapture(0); ok,_=c.read(); print(ok); c.release()"Should print
True. -
If
False, check the camera isn't held by another process:fuser /dev/video0 # shows PID using it -
Permission check — your user should be in the
videogroup:groups # look for "video" sudo usermod -aG video $USER # add if missing; log out + back in
-
Try a different
CAMERA_INDEX(some laptops list the same camera as both 0 and 1).
ANTHROPIC_API_KEY not set / API errors on every fire
Either the .env file isn't being picked up, or the key in it is wrong:
cd ~/crowbuster cat .env | grep ANTHROPIC # key should start with sk-ant- python3 -c "from dotenv import load_dotenv; load_dotenv(); import os; print('key set:', bool(os.environ.get('ANTHROPIC_API_KEY')))"
If key set: False, the file is missing or malformed. Recreate it from .env.example.
False positives: speaker keeps firing on an empty porch
-
Look at the most recent capture to see what triggered it:
ls -lt captures/ | head -5Then
scpit back to look at it. Common culprits: a coat on a chair, a poster of a person, your reflection in a window, the laundry on a line. -
If YOLO is hallucinating a
birdon an empty frame, raise its confidence threshold:YOLO_BIRD_CONFIDENCE = 0.40 # was 0.25
-
If motion is firing too often (camera shake, lighting changes), raise:
MOTION_THRESHOLD = 6.0 # was 3.5
The default is intentionally permissive — false motion just costs a YOLO call (~140ms, free). Only raise if YOLO is being woken constantly on an empty frame.
False negatives: real crow visited but no fire happened
- Check
events.logfor the time window — did motion fire? did YOLO escalate? - If motion didn't fire, lower
MOTION_THRESHOLD(try2.5, then2.0). Default is3.5; small or distant birds against a still porch may only nudge the mean diff by 2–3. Even at the default,YOLO_FORCE_CHECK_EVERY=30runs YOLO every 30th frame regardless — so a totally silent landing should still get caught within ~30s. If you're seeingmotion=0for long stretches in the stats line during clearly-active daylight hours, drop the threshold. - If motion fired but YOLO didn't find a bird, lower
YOLO_BIRD_CONFIDENCE. - If both fired but Claude said no, try upgrading the model:
MODEL = "claude-opus-4-7" # more accurate, ~5× the cost
NNPACK could not initialize warnings flood the log
Suppressed by default on newer code (the YOLO call is wrapped in _silenced_stderr). If you still see them, you're likely on an older revision — git pull to update.
Disk filling up
captures/is capped atMAX_CAPTURES(default 500). Lower it if needed.events.loggrows ~75 MB/year. Truncate without restarting:: > ~/crowbuster/events.log
- PyTorch CUDA libs eat ~3 GB on a CPU-only laptop. Reclaim:
source .venv/bin/activate pip uninstall -y torch torchvision nvidia-* triton cuda-* pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu
The script crashed, how do I see what happened?
tail -100 ~/crowbuster/events.logIf the process died without writing the stack trace, you may need to look at stdout where the script was launched, or the cron output file if running via @reboot.
Verifying the script is running (from another machine)
ssh utkarsh@192.168.5.33 'cat ~/crowbuster/heartbeat'The timestamp should be within the last 60 seconds. Stale = script died, restart with:
ssh utkarsh@192.168.5.33 'cd ~/crowbuster && nohup .venv/bin/python crowbuster.py >> events.log 2>&1 &'Credits
- Crow distress audio: HME Products
- Local bird detection: Ultralytics YOLOv8
- Vision classification: Claude (
claude-haiku-4-5) - Built with the help of Claude Code
License
MIT — see LICENSE. Sound files in ./sounds/ retain their original rights from HME Products.