Apiarist Dev commited on
Commit Β·
238bdf6
1
Parent(s): 686d2ed
polish: remove emojis and em dashes from sources and docs
Browse files- BLOG_POST.md +5 -5
- DEMO_VIDEO_SCRIPT.md +20 -20
- LINKEDIN_POST.md +15 -15
- README.md +7 -7
- app.py +45 -45
- data/SOURCES.md +5 -5
- db.py +1 -1
- detector.py +10 -10
- report.py +4 -4
- scripts/download_yolo_weights.py +6 -6
- scripts/extract_dataset.py +2 -2
- scripts/push_dataset_to_hub.py +4 -4
- scripts/push_yolo_to_hub.py +9 -9
- scripts/scrape_inaturalist.py +7 -7
- scripts/train_yolo_on_modal.py +1 -1
BLOG_POST.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
| 6 |
|
| 7 |
## The constraint that made this interesting
|
| 8 |
|
| 9 |
-
The hackathon's only hard rule: **total parameters β€ 32 billion**, and the whole thing must run on a laptop. No cloud APIs at runtime. Build something that solves a real problem for a real person, then prove a small model is genuinely the right tool for the job
|
| 10 |
|
| 11 |
I built **Apiarist**: a fully-offline AI hive frame inspector for backyard beekeepers. You point a phone camera at any honeycomb frame and the app counts bees, flags the queen, spots varroa mites, and writes a narrative report you can actually log. No internet required.
|
| 12 |
|
|
@@ -14,7 +14,7 @@ Live demo: [huggingface.co/spaces/build-small-hackathon/Apiarist](https://huggin
|
|
| 14 |
|
| 15 |
## Why beekeeping, why now
|
| 16 |
|
| 17 |
-
Frank
|
| 18 |
|
| 19 |
The problem is real but tiny: he's not going to upload private hive data to a cloud API, and his apiary has no signal anyway. He doesn't need a foundation model. He needs **a fast, focused tool that runs on his laptop in a field.**
|
| 20 |
|
|
@@ -31,13 +31,13 @@ The trick: each model does what it's actually good at.
|
|
| 31 |
|
| 32 |
Vision-language models are terrible at counting tiny things or picking subtle queens out of a sea of nearly-identical workers. Ask Qwen-3B "how many varroa mites?" and it will confidently hallucinate a number. Ask a specialist YOLO and you get a real count grounded in pixels.
|
| 33 |
|
| 34 |
-
Inversely, YOLO can't write a useful report. "47 bees, 1 queen, 0 mites" is data, not language. Qwen takes those facts and produces "Healthy frame, queen present, brood pattern looks solid
|
| 35 |
|
| 36 |
Together: structured + narrative. That's a real product.
|
| 37 |
|
| 38 |
## Training a YOLO on Modal in one afternoon
|
| 39 |
|
| 40 |
-
The first dataset I tried (Matt Nudi's 909-image honey-bee set) had only ~50 queen examples. Queen mAP came out at 0.37 with P=0.19
|
| 41 |
|
| 42 |
Pivoted to **hendricks_ricky/bee-project** on Roboflow Universe: 3,308 labeled images with 892 queens AND a Varroa class. Trained YOLOv8s for 60 epochs on a single Modal T4 in ~50 minutes for ~$0.40 of free hackathon credit.
|
| 43 |
|
|
@@ -59,7 +59,7 @@ The only network calls are *downloading* the models on first boot. After that, i
|
|
| 59 |
|
| 60 |
- **More queen training data.** 892 examples is the most I could find, but the model still misses queens that aren't well-lit or facing the camera.
|
| 61 |
- **A second specialist for swarm cells.** Right now those are detected by Qwen-3B's prose, which means false alarms.
|
| 62 |
-
- **Voice notes**, with Whisper transcription. Beekeepers wear gloves
|
| 63 |
- **Bee count trends over time** in the PDF report. Pretty graphs sell.
|
| 64 |
|
| 65 |
## The thing I learned
|
|
|
|
| 6 |
|
| 7 |
## The constraint that made this interesting
|
| 8 |
|
| 9 |
+
The hackathon's only hard rule: **total parameters β€ 32 billion**, and the whole thing must run on a laptop. No cloud APIs at runtime. Build something that solves a real problem for a real person, then prove a small model is genuinely the right tool for the job, not a cop-out for not having GPT-4o's API key.
|
| 10 |
|
| 11 |
I built **Apiarist**: a fully-offline AI hive frame inspector for backyard beekeepers. You point a phone camera at any honeycomb frame and the app counts bees, flags the queen, spots varroa mites, and writes a narrative report you can actually log. No internet required.
|
| 12 |
|
|
|
|
| 14 |
|
| 15 |
## Why beekeeping, why now
|
| 16 |
|
| 17 |
+
Frank, a beekeeper a few streets over, keeps 14 hives. Every weekend he inspects each frame and writes notes by hand in a beat-up notebook. Last summer he lost four colonies because he didn't spot a queen failure in time.
|
| 18 |
|
| 19 |
The problem is real but tiny: he's not going to upload private hive data to a cloud API, and his apiary has no signal anyway. He doesn't need a foundation model. He needs **a fast, focused tool that runs on his laptop in a field.**
|
| 20 |
|
|
|
|
| 31 |
|
| 32 |
Vision-language models are terrible at counting tiny things or picking subtle queens out of a sea of nearly-identical workers. Ask Qwen-3B "how many varroa mites?" and it will confidently hallucinate a number. Ask a specialist YOLO and you get a real count grounded in pixels.
|
| 33 |
|
| 34 |
+
Inversely, YOLO can't write a useful report. "47 bees, 1 queen, 0 mites" is data, not language. Qwen takes those facts and produces "Healthy frame, queen present, brood pattern looks solid, no swarm prep visible."
|
| 35 |
|
| 36 |
Together: structured + narrative. That's a real product.
|
| 37 |
|
| 38 |
## Training a YOLO on Modal in one afternoon
|
| 39 |
|
| 40 |
+
The first dataset I tried (Matt Nudi's 909-image honey-bee set) had only ~50 queen examples. Queen mAP came out at 0.37 with P=0.19, basically useless for queen detection.
|
| 41 |
|
| 42 |
Pivoted to **hendricks_ricky/bee-project** on Roboflow Universe: 3,308 labeled images with 892 queens AND a Varroa class. Trained YOLOv8s for 60 epochs on a single Modal T4 in ~50 minutes for ~$0.40 of free hackathon credit.
|
| 43 |
|
|
|
|
| 59 |
|
| 60 |
- **More queen training data.** 892 examples is the most I could find, but the model still misses queens that aren't well-lit or facing the camera.
|
| 61 |
- **A second specialist for swarm cells.** Right now those are detected by Qwen-3B's prose, which means false alarms.
|
| 62 |
+
- **Voice notes**, with Whisper transcription. Beekeepers wear gloves, typing is bad UX in the field.
|
| 63 |
- **Bee count trends over time** in the PDF report. Pretty graphs sell.
|
| 64 |
|
| 65 |
## The thing I learned
|
DEMO_VIDEO_SCRIPT.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# Apiarist
|
| 2 |
|
| 3 |
**Total runtime:** 60 seconds
|
| 4 |
**Format:** Screen recording (your laptop) + voiceover. No face-on-camera needed.
|
|
@@ -9,47 +9,47 @@
|
|
| 9 |
|
| 10 |
## Shot list (timecoded)
|
| 11 |
|
| 12 |
-
### 0
|
| 13 |
**Visual:** Close-up still of a honeycomb frame from the dataset, full screen. Cut to a laptop sitting on a wooden surface (your desk works).
|
| 14 |
**Voiceover:**
|
| 15 |
> "Beekeepers inspect hives every weekend. Most still write notes in a beat-up notebook."
|
| 16 |
|
| 17 |
-
### 5
|
| 18 |
**Visual:** Browser tab opening to Apiarist Space. Pause on the welcome screen.
|
| 19 |
**Voiceover:**
|
| 20 |
-
> "Last summer, Frank
|
| 21 |
|
| 22 |
-
### 15
|
| 23 |
**Visual:** Screen recording.
|
| 24 |
1. Click **Inspect** tab
|
| 25 |
2. Pick **"Hive #7"** from dropdown
|
| 26 |
-
3. Upload a clean bee frame photo (use one with a visible queen
|
| 27 |
4. Click **Analyze Frame**
|
| 28 |
-
5. ~5 seconds of "thinking"
|
| 29 |
6. **Annotated frame appears** with bounding boxes
|
| 30 |
-
7. **Pan over the result panel**
|
| 31 |
|
| 32 |
**Voiceover (over the action):**
|
| 33 |
-
> "Snap a photo of any comb frame. Apiarist runs a custom-trained YOLO detector to find queens, drones, workers, and varroa mites
|
| 34 |
|
| 35 |
-
### 30
|
| 36 |
**Visual:** Switch to **Compare** tab. Upload same photo. Click "Run Comparison." Both sides analyze. Camera lingers on the side-by-side: clean structured output on the left (Apiarist), vague prose on the right (raw VLM).
|
| 37 |
**Voiceover:**
|
| 38 |
-
> "Compare a generic vision model on its own
|
| 39 |
|
| 40 |
-
### 42
|
| 41 |
**Visual:**
|
| 42 |
- Switch to **Hives** tab
|
| 43 |
- Show the apiary table with multiple hives
|
| 44 |
-
- Click into inspection history for one hive
|
| 45 |
-
- Click **"Generate PDF Report"**
|
| 46 |
|
| 47 |
**Voiceover:**
|
| 48 |
> "Every inspection auto-saves to that hive's log. Generate weekly PDF reports. Track which hives need attention. SQLite-backed, runs anywhere."
|
| 49 |
|
| 50 |
-
### 52
|
| 51 |
**Visual:** Apiarist logo full screen with text overlay:
|
| 52 |
-
>
|
| 53 |
> Custom YOLOv8s + Qwen2.5-VL-3B
|
| 54 |
> Fully offline. Built in 10 days.
|
| 55 |
> huggingface.co/spaces/build-small-hackathon/Apiarist
|
|
@@ -63,12 +63,12 @@
|
|
| 63 |
|
| 64 |
- [ ] Practice run the click-through 3 times before hitting record
|
| 65 |
- [ ] Pre-load 3-4 bee photos in a folder, picked because the model handles them well
|
| 66 |
-
- [ ] Pre-create at least 3 hives in the Hives tab with realistic names ("Hive #1
|
| 67 |
-
- [ ] Log 4
|
| 68 |
- [ ] Switch the Space to T4/ZeroGPU mode and prewarm by clicking Analyze once before recording (avoids 30s cold-start in the video)
|
| 69 |
-
- [ ] Record voiceover separately on phone in a quiet room
|
| 70 |
- [ ] Edit in CapCut / Premiere / DaVinci Resolve (all free for this)
|
| 71 |
-
- [ ] Add subtle background music (uncopyrighted
|
| 72 |
- [ ] Export as MP4 1920x1080 30fps, under 50MB
|
| 73 |
|
| 74 |
## What NOT to do
|
|
|
|
| 1 |
+
# Apiarist, 60s Demo Video Script
|
| 2 |
|
| 3 |
**Total runtime:** 60 seconds
|
| 4 |
**Format:** Screen recording (your laptop) + voiceover. No face-on-camera needed.
|
|
|
|
| 9 |
|
| 10 |
## Shot list (timecoded)
|
| 11 |
|
| 12 |
+
### 0-5s, Hook
|
| 13 |
**Visual:** Close-up still of a honeycomb frame from the dataset, full screen. Cut to a laptop sitting on a wooden surface (your desk works).
|
| 14 |
**Voiceover:**
|
| 15 |
> "Beekeepers inspect hives every weekend. Most still write notes in a beat-up notebook."
|
| 16 |
|
| 17 |
+
### 5-15s, Problem
|
| 18 |
**Visual:** Browser tab opening to Apiarist Space. Pause on the welcome screen.
|
| 19 |
**Voiceover:**
|
| 20 |
+
> "Last summer, Frank, a backyard beekeeper down the road, lost four colonies because he didn't spot queen failures in time. Apiarist is the assistant he should have had."
|
| 21 |
|
| 22 |
+
### 15-30s, The App In Action (THE PROOF)
|
| 23 |
**Visual:** Screen recording.
|
| 24 |
1. Click **Inspect** tab
|
| 25 |
2. Pick **"Hive #7"** from dropdown
|
| 26 |
+
3. Upload a clean bee frame photo (use one with a visible queen, pre-test which ones the model nails)
|
| 27 |
4. Click **Analyze Frame**
|
| 28 |
+
5. ~5 seconds of "thinking", cut this short in edit if needed
|
| 29 |
6. **Annotated frame appears** with bounding boxes
|
| 30 |
+
7. **Pan over the result panel**, show queen badge (bright green), bee counts, brood pattern, notes
|
| 31 |
|
| 32 |
**Voiceover (over the action):**
|
| 33 |
+
> "Snap a photo of any comb frame. Apiarist runs a custom-trained YOLO detector to find queens, drones, workers, and varroa mites, then a 3-billion-parameter vision-language model writes a narrative report. All local. No cloud APIs. No subscription."
|
| 34 |
|
| 35 |
+
### 30-42s, The Kill Shot (Compare tab)
|
| 36 |
**Visual:** Switch to **Compare** tab. Upload same photo. Click "Run Comparison." Both sides analyze. Camera lingers on the side-by-side: clean structured output on the left (Apiarist), vague prose on the right (raw VLM).
|
| 37 |
**Voiceover:**
|
| 38 |
+
> "Compare a generic vision model on its own, it describes the photo, but can't count bees or pick out the queen. Apiarist's specialist YOLO + small VLM combo gives you something a beekeeper can actually log."
|
| 39 |
|
| 40 |
+
### 42-52s, Workflow
|
| 41 |
**Visual:**
|
| 42 |
- Switch to **Hives** tab
|
| 43 |
- Show the apiary table with multiple hives
|
| 44 |
+
- Click into inspection history for one hive, show 4-5 past inspections
|
| 45 |
+
- Click **"Generate PDF Report"**, show the downloaded PDF preview
|
| 46 |
|
| 47 |
**Voiceover:**
|
| 48 |
> "Every inspection auto-saves to that hive's log. Generate weekly PDF reports. Track which hives need attention. SQLite-backed, runs anywhere."
|
| 49 |
|
| 50 |
+
### 52-60s, Close
|
| 51 |
**Visual:** Apiarist logo full screen with text overlay:
|
| 52 |
+
> Apiarist
|
| 53 |
> Custom YOLOv8s + Qwen2.5-VL-3B
|
| 54 |
> Fully offline. Built in 10 days.
|
| 55 |
> huggingface.co/spaces/build-small-hackathon/Apiarist
|
|
|
|
| 63 |
|
| 64 |
- [ ] Practice run the click-through 3 times before hitting record
|
| 65 |
- [ ] Pre-load 3-4 bee photos in a folder, picked because the model handles them well
|
| 66 |
+
- [ ] Pre-create at least 3 hives in the Hives tab with realistic names ("Hive #1, Northeast", "Hive #2, Apricot tree", "Hive #3, Backyard")
|
| 67 |
+
- [ ] Log 4-5 inspections per hive before recording (so the history view has content)
|
| 68 |
- [ ] Switch the Space to T4/ZeroGPU mode and prewarm by clicking Analyze once before recording (avoids 30s cold-start in the video)
|
| 69 |
+
- [ ] Record voiceover separately on phone in a quiet room, read each section twice, pick best take
|
| 70 |
- [ ] Edit in CapCut / Premiere / DaVinci Resolve (all free for this)
|
| 71 |
+
- [ ] Add subtle background music (uncopyrighted, try YouTube Audio Library "instrumental folk")
|
| 72 |
- [ ] Export as MP4 1920x1080 30fps, under 50MB
|
| 73 |
|
| 74 |
## What NOT to do
|
LINKEDIN_POST.md
CHANGED
|
@@ -6,23 +6,23 @@ Post this on submission day. Upload the demo video natively (LinkedIn throttles
|
|
| 6 |
|
| 7 |
I spent 10 days teaching a small AI to do something OpenAI's flagship can't: count bees on a honeycomb frame.
|
| 8 |
|
| 9 |
-
For the Build Small Hackathon by Hugging Face Γ Gradio, I built **Apiarist**
|
| 10 |
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
|
| 17 |
-
Why small models? Because apiaries don't have signal. Hive-health data isn't for the cloud. And a custom YOLOv8s trained on 3,308 labeled bee images, paired with Qwen2.5-VL-3B running locally on ZeroGPU, beats GPT-4o at counting bees
|
| 18 |
|
| 19 |
Specialist (YOLO) finds and counts the things. Generalist (VLM) writes the narrative report. Together: structured + narrative inspection in 5 seconds, no internet required.
|
| 20 |
|
| 21 |
Total params: ~3 billion. Total cost per inspection: zero.
|
| 22 |
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
|
| 27 |
Thanks to Hugging Face, Gradio, Modal, OpenAI, NVIDIA, Cohere, Black Forest Labs, and OpenBMB for the hackathon and the compute.
|
| 28 |
|
|
@@ -47,15 +47,15 @@ The names turn **bold blue** when they're real tags.
|
|
| 47 |
## Upload order
|
| 48 |
|
| 49 |
1. Open new LinkedIn post
|
| 50 |
-
2. **Upload demo video file directly** (not a YouTube link)
|
| 51 |
3. Type the post text above (with tags applied)
|
| 52 |
-
4. **Put the Space URL in the first comment**, not the post body
|
| 53 |
5. Post
|
| 54 |
|
| 55 |
## Backup channels
|
| 56 |
|
| 57 |
If you don't get traction on LinkedIn, cross-post to:
|
| 58 |
-
- Twitter / X (split into 3 tweets
|
| 59 |
- Bluesky (same as Twitter)
|
| 60 |
-
- Reddit r/beekeeping (separate post, lead with "I built an AI hive inspector
|
| 61 |
-
- Reddit r/LocalLLaMA (lead with the tech story
|
|
|
|
| 6 |
|
| 7 |
I spent 10 days teaching a small AI to do something OpenAI's flagship can't: count bees on a honeycomb frame.
|
| 8 |
|
| 9 |
+
For the Build Small Hackathon by Hugging Face Γ Gradio, I built **Apiarist**, a fully offline AI hive inspector for backyard beekeepers. Point a phone at any comb frame and it tells you:
|
| 10 |
|
| 11 |
+
how many bees, drones, and queen candidates it sees
|
| 12 |
+
where the queen is (with a green box around her)
|
| 13 |
+
whether varroa mites are visible
|
| 14 |
+
what the brood pattern looks like
|
| 15 |
+
generates a weekly PDF inspection report
|
| 16 |
|
| 17 |
+
Why small models? Because apiaries don't have signal. Hive-health data isn't for the cloud. And a custom YOLOv8s trained on 3,308 labeled bee images, paired with Qwen2.5-VL-3B running locally on ZeroGPU, beats GPT-4o at counting bees, because that's literally what it was designed for.
|
| 18 |
|
| 19 |
Specialist (YOLO) finds and counts the things. Generalist (VLM) writes the narrative report. Together: structured + narrative inspection in 5 seconds, no internet required.
|
| 20 |
|
| 21 |
Total params: ~3 billion. Total cost per inspection: zero.
|
| 22 |
|
| 23 |
+
Demo video below
|
| 24 |
+
Try it: huggingface.co/spaces/build-small-hackathon/Apiarist
|
| 25 |
+
Field notes: [link to your blog post once published]
|
| 26 |
|
| 27 |
Thanks to Hugging Face, Gradio, Modal, OpenAI, NVIDIA, Cohere, Black Forest Labs, and OpenBMB for the hackathon and the compute.
|
| 28 |
|
|
|
|
| 47 |
## Upload order
|
| 48 |
|
| 49 |
1. Open new LinkedIn post
|
| 50 |
+
2. **Upload demo video file directly** (not a YouTube link), LinkedIn favors native video
|
| 51 |
3. Type the post text above (with tags applied)
|
| 52 |
+
4. **Put the Space URL in the first comment**, not the post body, LinkedIn throttles posts with external links in the body
|
| 53 |
5. Post
|
| 54 |
|
| 55 |
## Backup channels
|
| 56 |
|
| 57 |
If you don't get traction on LinkedIn, cross-post to:
|
| 58 |
+
- Twitter / X (split into 3 tweets, hook + features + link)
|
| 59 |
- Bluesky (same as Twitter)
|
| 60 |
+
- Reddit r/beekeeping (separate post, lead with "I built an AI hive inspector, would love your feedback")
|
| 61 |
+
- Reddit r/LocalLLaMA (lead with the tech story, specialist+generalist, 3B params, ZeroGPU)
|
README.md
CHANGED
|
@@ -23,7 +23,7 @@ datasets:
|
|
| 23 |
- maryammeda/apiarist-inaturalist-bees
|
| 24 |
---
|
| 25 |
|
| 26 |
-
#
|
| 27 |
|
| 28 |
A fully-offline AI hive frame inspector for backyard beekeepers. Built
|
| 29 |
in 10 days for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
|
@@ -50,15 +50,15 @@ All local. No cloud APIs at inference.
|
|
| 50 |
|
| 51 |
## Stack
|
| 52 |
|
| 53 |
-
-
|
| 54 |
-
-
|
| 55 |
-
-
|
| 56 |
-
-
|
| 57 |
-
-
|
| 58 |
|
| 59 |
## Badges chased
|
| 60 |
|
| 61 |
-
|
| 62 |
|
| 63 |
## License
|
| 64 |
|
|
|
|
| 23 |
- maryammeda/apiarist-inaturalist-bees
|
| 24 |
---
|
| 25 |
|
| 26 |
+
# Apiarist
|
| 27 |
|
| 28 |
A fully-offline AI hive frame inspector for backyard beekeepers. Built
|
| 29 |
in 10 days for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
|
|
|
| 50 |
|
| 51 |
## Stack
|
| 52 |
|
| 53 |
+
- [Hugging Face Spaces](https://huggingface.co/spaces) on ZeroGPU
|
| 54 |
+
- [Gradio](https://gradio.app) with a custom field-tool theme
|
| 55 |
+
- [Modal](https://modal.com) for fine-tuning the YOLO (1Γ T4, 60 epochs)
|
| 56 |
+
- Training data: [Hendricks Ricky bee-project](https://universe.roboflow.com/hendricks_ricky-hotmail-de/bee-project) (3,308 imgs, 892 queens)
|
| 57 |
+
- Context imagery: [Apiarist iNaturalist bees dataset](https://huggingface.co/datasets/maryammeda/apiarist-inaturalist-bees)
|
| 58 |
|
| 59 |
## Badges chased
|
| 60 |
|
| 61 |
+
Off the Grid Β· Well-Tuned Β· Off-Brand Β· Sharing is Caring Β· Field Notes
|
| 62 |
|
| 63 |
## License
|
| 64 |
|
app.py
CHANGED
|
@@ -23,7 +23,7 @@ import db
|
|
| 23 |
import detector
|
| 24 |
import report
|
| 25 |
|
| 26 |
-
# ZeroGPU integration
|
| 27 |
try:
|
| 28 |
import spaces
|
| 29 |
|
|
@@ -99,12 +99,12 @@ def parse_response(text: str, hive_name: str) -> dict:
|
|
| 99 |
|
| 100 |
def build_narrative(r: dict, raw: str) -> str:
|
| 101 |
queen_line = (
|
| 102 |
-
"
|
| 103 |
)
|
| 104 |
swarm_line = (
|
| 105 |
-
"
|
| 106 |
if r["swarm_cells_detected"]
|
| 107 |
-
else "
|
| 108 |
)
|
| 109 |
|
| 110 |
yolo_block = ""
|
|
@@ -113,7 +113,7 @@ def build_narrative(r: dict, raw: str) -> str:
|
|
| 113 |
# Show meaningful counts only
|
| 114 |
ordered = ["queen", "drone", "bee", "pollenbee"]
|
| 115 |
lines = [
|
| 116 |
-
f"
|
| 117 |
]
|
| 118 |
yolo_block = "\n**Specialist detector counts:**\n" + "\n".join(lines)
|
| 119 |
footer = "*Powered by custom YOLOv8s + Qwen2.5-VL-3B on ZeroGPU.*"
|
|
@@ -122,7 +122,7 @@ def build_narrative(r: dict, raw: str) -> str:
|
|
| 122 |
|
| 123 |
# Mite count is Qwen-only (no specialist), flag that clearly
|
| 124 |
mite_line = (
|
| 125 |
-
f"
|
| 126 |
)
|
| 127 |
|
| 128 |
return f"""**Hive: {r['hive']}**
|
|
@@ -132,7 +132,7 @@ def build_narrative(r: dict, raw: str) -> str:
|
|
| 132 |
{mite_line}
|
| 133 |
{yolo_block}
|
| 134 |
|
| 135 |
-
**Brood pattern:** {r['brood_pattern']}
|
| 136 |
**Notes:** {r['notes']}
|
| 137 |
|
| 138 |
---
|
|
@@ -285,12 +285,12 @@ def run_comparison(image: Image.Image):
|
|
| 285 |
left_results["queen_detected"] = counts.get("queen", 0) > 0
|
| 286 |
left_results["yolo_counts"] = counts
|
| 287 |
left_results["detector_used"] = True
|
| 288 |
-
left_md = _short_summary("
|
| 289 |
left_image = annotated if annotated is not None else image
|
| 290 |
else:
|
| 291 |
left_md = (
|
| 292 |
-
"###
|
| 293 |
-
"*Custom YOLO weights haven't landed yet
|
| 294 |
"left column will activate as soon as `weights/honey_bee_detector.pt` is committed.*"
|
| 295 |
)
|
| 296 |
left_image = image
|
|
@@ -299,16 +299,16 @@ def run_comparison(image: Image.Image):
|
|
| 299 |
right_response = _run_qwen(image, prefix="")
|
| 300 |
right_results = parse_response(right_response, "")
|
| 301 |
right_results["detector_used"] = False
|
| 302 |
-
right_md = _short_summary("
|
| 303 |
|
| 304 |
return left_image, left_md, image, right_md
|
| 305 |
|
| 306 |
|
| 307 |
def _short_summary(title: str, r: dict, extra_counts: dict | None = None) -> str:
|
| 308 |
-
queen = "
|
| 309 |
-
mite_icon = "
|
| 310 |
swarm = (
|
| 311 |
-
"
|
| 312 |
)
|
| 313 |
counts_block = ""
|
| 314 |
if extra_counts:
|
|
@@ -323,7 +323,7 @@ def _short_summary(title: str, r: dict, extra_counts: dict | None = None) -> str
|
|
| 323 |
{swarm}
|
| 324 |
{counts_block}
|
| 325 |
|
| 326 |
-
**Brood:** {r['brood_pattern']}
|
| 327 |
**Notes:** {r['notes']}
|
| 328 |
"""
|
| 329 |
|
|
@@ -338,7 +338,7 @@ def _hives_table_state() -> list[list]:
|
|
| 338 |
last = (
|
| 339 |
time.strftime("%Y-%m-%d %H:%M", time.localtime(h["last_inspected"]))
|
| 340 |
if h["last_inspected"]
|
| 341 |
-
else "
|
| 342 |
)
|
| 343 |
out.append(
|
| 344 |
[
|
|
@@ -359,13 +359,13 @@ def add_hive_action(name, location, marker, notes):
|
|
| 359 |
_hives_table_state(),
|
| 360 |
gr.update(),
|
| 361 |
gr.update(),
|
| 362 |
-
"
|
| 363 |
)
|
| 364 |
try:
|
| 365 |
db.add_hive(name, location or "", marker or "", notes or "")
|
| 366 |
-
msg = f"
|
| 367 |
except sqlite3.IntegrityError:
|
| 368 |
-
msg = f"
|
| 369 |
hive_names = [h["name"] for h in db.list_hives()]
|
| 370 |
return (
|
| 371 |
_hives_table_state(),
|
|
@@ -457,13 +457,13 @@ def _seed_sample_data():
|
|
| 457 |
"""Drop a handful of plausible hives into the DB on first boot
|
| 458 |
so the Hives tab isn't empty for first-visit judges."""
|
| 459 |
if db.list_hives():
|
| 460 |
-
return
|
| 461 |
samples = [
|
| 462 |
-
("Hive #1
|
| 463 |
"Italian queen, marked yellow"),
|
| 464 |
-
("Hive #2
|
| 465 |
"2024 queen, watch for supersedure cells"),
|
| 466 |
-
("Hive #3
|
| 467 |
"Newly split from Hive #1 in May"),
|
| 468 |
]
|
| 469 |
for name, loc, marker, notes in samples:
|
|
@@ -478,7 +478,7 @@ def build_ui() -> gr.Blocks:
|
|
| 478 |
_seed_sample_data()
|
| 479 |
|
| 480 |
with gr.Blocks(title="Apiarist - Hive Frame Inspector") as app:
|
| 481 |
-
gr.Markdown("#
|
| 482 |
gr.Markdown(
|
| 483 |
"*Offline AI inspector for honeybee hive frames. "
|
| 484 |
"Built for the Build Small Hackathon.*"
|
|
@@ -486,7 +486,7 @@ def build_ui() -> gr.Blocks:
|
|
| 486 |
|
| 487 |
with gr.Tabs():
|
| 488 |
# ------- INSPECT TAB -------
|
| 489 |
-
with gr.Tab("
|
| 490 |
with gr.Row():
|
| 491 |
with gr.Column():
|
| 492 |
hive_input = gr.Dropdown(
|
|
@@ -501,7 +501,7 @@ def build_ui() -> gr.Blocks:
|
|
| 501 |
sources=["upload", "webcam"],
|
| 502 |
)
|
| 503 |
analyze_btn = gr.Button(
|
| 504 |
-
"
|
| 505 |
)
|
| 506 |
with gr.Column():
|
| 507 |
annotated_output = gr.Image(label="Annotated Frame")
|
|
@@ -510,7 +510,7 @@ def build_ui() -> gr.Blocks:
|
|
| 510 |
json_output = gr.Code(language="json")
|
| 511 |
|
| 512 |
# ------- HIVES TAB -------
|
| 513 |
-
with gr.Tab("
|
| 514 |
with gr.Row():
|
| 515 |
with gr.Column(scale=1):
|
| 516 |
gr.Markdown("### Add a Hive")
|
|
@@ -529,7 +529,7 @@ def build_ui() -> gr.Blocks:
|
|
| 529 |
new_notes = gr.Textbox(
|
| 530 |
label="Notes (optional)", lines=2
|
| 531 |
)
|
| 532 |
-
add_btn = gr.Button("
|
| 533 |
add_msg = gr.Markdown()
|
| 534 |
|
| 535 |
with gr.Column(scale=2):
|
|
@@ -544,7 +544,7 @@ def build_ui() -> gr.Blocks:
|
|
| 544 |
value=_hives_table_state(),
|
| 545 |
wrap=True,
|
| 546 |
)
|
| 547 |
-
refresh_btn = gr.Button("
|
| 548 |
|
| 549 |
gr.Markdown("---\n### Inspection history")
|
| 550 |
history_select = gr.Dropdown(
|
|
@@ -565,20 +565,20 @@ def build_ui() -> gr.Blocks:
|
|
| 565 |
gr.Markdown("---\n### Weekly report")
|
| 566 |
with gr.Row():
|
| 567 |
report_btn = gr.Button(
|
| 568 |
-
"
|
| 569 |
)
|
| 570 |
report_file = gr.File(
|
| 571 |
label="Latest report", interactive=False
|
| 572 |
)
|
| 573 |
|
| 574 |
# ------- COMPARE TAB -------
|
| 575 |
-
with gr.Tab("
|
| 576 |
gr.Markdown(
|
| 577 |
"### Apiarist vs raw generalist VLM\n"
|
| 578 |
"Same image, two pipelines. Apiarist combines a "
|
| 579 |
"**custom-trained YOLO specialist** with a generalist VLM. "
|
| 580 |
"The right column shows what happens when you ask the "
|
| 581 |
-
"generalist alone
|
| 582 |
)
|
| 583 |
with gr.Row():
|
| 584 |
cmp_image = gr.Image(
|
|
@@ -587,33 +587,33 @@ def build_ui() -> gr.Blocks:
|
|
| 587 |
sources=["upload", "webcam"],
|
| 588 |
)
|
| 589 |
cmp_btn = gr.Button(
|
| 590 |
-
"
|
| 591 |
)
|
| 592 |
|
| 593 |
with gr.Row():
|
| 594 |
with gr.Column():
|
| 595 |
-
gr.Markdown("###
|
| 596 |
cmp_left_image = gr.Image(
|
| 597 |
label="Annotated by YOLO + Qwen"
|
| 598 |
)
|
| 599 |
cmp_left_text = gr.Markdown()
|
| 600 |
with gr.Column():
|
| 601 |
-
gr.Markdown("###
|
| 602 |
cmp_right_image = gr.Image(label="No annotations")
|
| 603 |
cmp_right_text = gr.Markdown()
|
| 604 |
|
| 605 |
# ------- ABOUT TAB -------
|
| 606 |
-
with gr.Tab("
|
| 607 |
gr.Markdown(
|
| 608 |
"""
|
| 609 |
-
##
|
| 610 |
|
| 611 |
An offline AI hive frame inspector for backyard beekeepers. Built in
|
| 612 |
10 days for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 613 |
|
| 614 |
### The problem
|
| 615 |
|
| 616 |
-
Frank
|
| 617 |
weekend by hand. Last summer he lost four colonies to queen failures
|
| 618 |
he didn't spot in time. Apiarist is the assistant he should have had:
|
| 619 |
fast, focused, runs on a laptop in a field, never sends his hive data
|
|
@@ -623,9 +623,9 @@ anywhere.
|
|
| 623 |
|
| 624 |
Two models, two jobs:
|
| 625 |
|
| 626 |
-
- **YOLOv8s, custom-trained** (22 MB)
|
| 627 |
varroa mites with bounding boxes. CPU-fast.
|
| 628 |
-
- **Qwen2.5-VL-3B-Instruct**
|
| 629 |
and writes a narrative inspection report. Runs on ZeroGPU.
|
| 630 |
|
| 631 |
Together: structured detection + narrative report in ~5 seconds, no
|
|
@@ -633,10 +633,10 @@ internet required.
|
|
| 633 |
|
| 634 |
### What's in the box
|
| 635 |
|
| 636 |
-
-
|
| 637 |
-
-
|
| 638 |
-
-
|
| 639 |
-
-
|
| 640 |
|
| 641 |
### Stack
|
| 642 |
|
|
@@ -649,7 +649,7 @@ internet required.
|
|
| 649 |
|
| 650 |
### Badges chased
|
| 651 |
|
| 652 |
-
|
| 653 |
"""
|
| 654 |
)
|
| 655 |
|
|
|
|
| 23 |
import detector
|
| 24 |
import report
|
| 25 |
|
| 26 |
+
# ZeroGPU integration, no-op outside HF Spaces
|
| 27 |
try:
|
| 28 |
import spaces
|
| 29 |
|
|
|
|
| 99 |
|
| 100 |
def build_narrative(r: dict, raw: str) -> str:
|
| 101 |
queen_line = (
|
| 102 |
+
" Queen detected" if r["queen_detected"] else " No queen visible"
|
| 103 |
)
|
| 104 |
swarm_line = (
|
| 105 |
+
" Swarm cells (VLM estimate)"
|
| 106 |
if r["swarm_cells_detected"]
|
| 107 |
+
else " No swarm cells (VLM estimate)"
|
| 108 |
)
|
| 109 |
|
| 110 |
yolo_block = ""
|
|
|
|
| 113 |
# Show meaningful counts only
|
| 114 |
ordered = ["queen", "drone", "bee", "pollenbee"]
|
| 115 |
lines = [
|
| 116 |
+
f" β’ **{counts[c]}** {c}" for c in ordered if counts.get(c)
|
| 117 |
]
|
| 118 |
yolo_block = "\n**Specialist detector counts:**\n" + "\n".join(lines)
|
| 119 |
footer = "*Powered by custom YOLOv8s + Qwen2.5-VL-3B on ZeroGPU.*"
|
|
|
|
| 122 |
|
| 123 |
# Mite count is Qwen-only (no specialist), flag that clearly
|
| 124 |
mite_line = (
|
| 125 |
+
f" ~{r['varroa_mites_visible']} mite(s) (VLM estimate, not specialist)"
|
| 126 |
)
|
| 127 |
|
| 128 |
return f"""**Hive: {r['hive']}**
|
|
|
|
| 132 |
{mite_line}
|
| 133 |
{yolo_block}
|
| 134 |
|
| 135 |
+
**Brood pattern:** {r['brood_pattern']} | **Overall:** {r['frame_health']}
|
| 136 |
**Notes:** {r['notes']}
|
| 137 |
|
| 138 |
---
|
|
|
|
| 285 |
left_results["queen_detected"] = counts.get("queen", 0) > 0
|
| 286 |
left_results["yolo_counts"] = counts
|
| 287 |
left_results["detector_used"] = True
|
| 288 |
+
left_md = _short_summary(" Apiarist", left_results, extra_counts=counts)
|
| 289 |
left_image = annotated if annotated is not None else image
|
| 290 |
else:
|
| 291 |
left_md = (
|
| 292 |
+
"### Apiarist\n\n"
|
| 293 |
+
"*Custom YOLO weights haven't landed yet, "
|
| 294 |
"left column will activate as soon as `weights/honey_bee_detector.pt` is committed.*"
|
| 295 |
)
|
| 296 |
left_image = image
|
|
|
|
| 299 |
right_response = _run_qwen(image, prefix="")
|
| 300 |
right_results = parse_response(right_response, "")
|
| 301 |
right_results["detector_used"] = False
|
| 302 |
+
right_md = _short_summary(" Qwen alone", right_results)
|
| 303 |
|
| 304 |
return left_image, left_md, image, right_md
|
| 305 |
|
| 306 |
|
| 307 |
def _short_summary(title: str, r: dict, extra_counts: dict | None = None) -> str:
|
| 308 |
+
queen = " Queen detected" if r["queen_detected"] else " No queen visible"
|
| 309 |
+
mite_icon = "" if r["varroa_mites_visible"] >= 3 else ""
|
| 310 |
swarm = (
|
| 311 |
+
" Swarm cells" if r["swarm_cells_detected"] else " No swarm cells"
|
| 312 |
)
|
| 313 |
counts_block = ""
|
| 314 |
if extra_counts:
|
|
|
|
| 323 |
{swarm}
|
| 324 |
{counts_block}
|
| 325 |
|
| 326 |
+
**Brood:** {r['brood_pattern']} **Health:** {r['frame_health']}
|
| 327 |
**Notes:** {r['notes']}
|
| 328 |
"""
|
| 329 |
|
|
|
|
| 338 |
last = (
|
| 339 |
time.strftime("%Y-%m-%d %H:%M", time.localtime(h["last_inspected"]))
|
| 340 |
if h["last_inspected"]
|
| 341 |
+
else "-"
|
| 342 |
)
|
| 343 |
out.append(
|
| 344 |
[
|
|
|
|
| 359 |
_hives_table_state(),
|
| 360 |
gr.update(),
|
| 361 |
gr.update(),
|
| 362 |
+
" Name required.",
|
| 363 |
)
|
| 364 |
try:
|
| 365 |
db.add_hive(name, location or "", marker or "", notes or "")
|
| 366 |
+
msg = f" Added hive '{name}'."
|
| 367 |
except sqlite3.IntegrityError:
|
| 368 |
+
msg = f" Hive '{name}' already exists."
|
| 369 |
hive_names = [h["name"] for h in db.list_hives()]
|
| 370 |
return (
|
| 371 |
_hives_table_state(),
|
|
|
|
| 457 |
"""Drop a handful of plausible hives into the DB on first boot
|
| 458 |
so the Hives tab isn't empty for first-visit judges."""
|
| 459 |
if db.list_hives():
|
| 460 |
+
return # someone already has data
|
| 461 |
samples = [
|
| 462 |
+
("Hive #1, Apricot tree", "South corner of yard", "yellow",
|
| 463 |
"Italian queen, marked yellow"),
|
| 464 |
+
("Hive #2, Cedar fence", "East side, near cedar", "red",
|
| 465 |
"2024 queen, watch for supersedure cells"),
|
| 466 |
+
("Hive #3, Vegetable garden", "By the tomato beds", "white",
|
| 467 |
"Newly split from Hive #1 in May"),
|
| 468 |
]
|
| 469 |
for name, loc, marker, notes in samples:
|
|
|
|
| 478 |
_seed_sample_data()
|
| 479 |
|
| 480 |
with gr.Blocks(title="Apiarist - Hive Frame Inspector") as app:
|
| 481 |
+
gr.Markdown("# APIARIST")
|
| 482 |
gr.Markdown(
|
| 483 |
"*Offline AI inspector for honeybee hive frames. "
|
| 484 |
"Built for the Build Small Hackathon.*"
|
|
|
|
| 486 |
|
| 487 |
with gr.Tabs():
|
| 488 |
# ------- INSPECT TAB -------
|
| 489 |
+
with gr.Tab(" Inspect"):
|
| 490 |
with gr.Row():
|
| 491 |
with gr.Column():
|
| 492 |
hive_input = gr.Dropdown(
|
|
|
|
| 501 |
sources=["upload", "webcam"],
|
| 502 |
)
|
| 503 |
analyze_btn = gr.Button(
|
| 504 |
+
" Analyze Frame", variant="primary"
|
| 505 |
)
|
| 506 |
with gr.Column():
|
| 507 |
annotated_output = gr.Image(label="Annotated Frame")
|
|
|
|
| 510 |
json_output = gr.Code(language="json")
|
| 511 |
|
| 512 |
# ------- HIVES TAB -------
|
| 513 |
+
with gr.Tab(" Hives") as hives_tab:
|
| 514 |
with gr.Row():
|
| 515 |
with gr.Column(scale=1):
|
| 516 |
gr.Markdown("### Add a Hive")
|
|
|
|
| 529 |
new_notes = gr.Textbox(
|
| 530 |
label="Notes (optional)", lines=2
|
| 531 |
)
|
| 532 |
+
add_btn = gr.Button(" Add Hive", variant="primary")
|
| 533 |
add_msg = gr.Markdown()
|
| 534 |
|
| 535 |
with gr.Column(scale=2):
|
|
|
|
| 544 |
value=_hives_table_state(),
|
| 545 |
wrap=True,
|
| 546 |
)
|
| 547 |
+
refresh_btn = gr.Button(" Refresh")
|
| 548 |
|
| 549 |
gr.Markdown("---\n### Inspection history")
|
| 550 |
history_select = gr.Dropdown(
|
|
|
|
| 565 |
gr.Markdown("---\n### Weekly report")
|
| 566 |
with gr.Row():
|
| 567 |
report_btn = gr.Button(
|
| 568 |
+
" Generate PDF report", variant="primary"
|
| 569 |
)
|
| 570 |
report_file = gr.File(
|
| 571 |
label="Latest report", interactive=False
|
| 572 |
)
|
| 573 |
|
| 574 |
# ------- COMPARE TAB -------
|
| 575 |
+
with gr.Tab(" Compare"):
|
| 576 |
gr.Markdown(
|
| 577 |
"### Apiarist vs raw generalist VLM\n"
|
| 578 |
"Same image, two pipelines. Apiarist combines a "
|
| 579 |
"**custom-trained YOLO specialist** with a generalist VLM. "
|
| 580 |
"The right column shows what happens when you ask the "
|
| 581 |
+
"generalist alone, same model, no specialist."
|
| 582 |
)
|
| 583 |
with gr.Row():
|
| 584 |
cmp_image = gr.Image(
|
|
|
|
| 587 |
sources=["upload", "webcam"],
|
| 588 |
)
|
| 589 |
cmp_btn = gr.Button(
|
| 590 |
+
" Run Comparison", variant="primary", scale=0
|
| 591 |
)
|
| 592 |
|
| 593 |
with gr.Row():
|
| 594 |
with gr.Column():
|
| 595 |
+
gr.Markdown("### Apiarist (specialist + generalist)")
|
| 596 |
cmp_left_image = gr.Image(
|
| 597 |
label="Annotated by YOLO + Qwen"
|
| 598 |
)
|
| 599 |
cmp_left_text = gr.Markdown()
|
| 600 |
with gr.Column():
|
| 601 |
+
gr.Markdown("### Raw generalist (Qwen alone)")
|
| 602 |
cmp_right_image = gr.Image(label="No annotations")
|
| 603 |
cmp_right_text = gr.Markdown()
|
| 604 |
|
| 605 |
# ------- ABOUT TAB -------
|
| 606 |
+
with gr.Tab("About"):
|
| 607 |
gr.Markdown(
|
| 608 |
"""
|
| 609 |
+
## Apiarist
|
| 610 |
|
| 611 |
An offline AI hive frame inspector for backyard beekeepers. Built in
|
| 612 |
10 days for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 613 |
|
| 614 |
### The problem
|
| 615 |
|
| 616 |
+
Frank, a beekeeper down the road, keeps 14 hives and inspects each
|
| 617 |
weekend by hand. Last summer he lost four colonies to queen failures
|
| 618 |
he didn't spot in time. Apiarist is the assistant he should have had:
|
| 619 |
fast, focused, runs on a laptop in a field, never sends his hive data
|
|
|
|
| 623 |
|
| 624 |
Two models, two jobs:
|
| 625 |
|
| 626 |
+
- **YOLOv8s, custom-trained** (22 MB), finds bees, drones, queens, and
|
| 627 |
varroa mites with bounding boxes. CPU-fast.
|
| 628 |
+
- **Qwen2.5-VL-3B-Instruct**, takes the image + YOLO detection counts
|
| 629 |
and writes a narrative inspection report. Runs on ZeroGPU.
|
| 630 |
|
| 631 |
Together: structured detection + narrative report in ~5 seconds, no
|
|
|
|
| 633 |
|
| 634 |
### What's in the box
|
| 635 |
|
| 636 |
+
- **Inspect**, upload a frame, get an annotated image + report
|
| 637 |
+
- **Hives**, registry, per-hive inspection history, weekly PDF reports
|
| 638 |
+
- **Compare**, Apiarist vs raw generalist VLM, side-by-side
|
| 639 |
+
- SQLite persistence for the whole apiary
|
| 640 |
|
| 641 |
### Stack
|
| 642 |
|
|
|
|
| 649 |
|
| 650 |
### Badges chased
|
| 651 |
|
| 652 |
+
Off the Grid Β· Well-Tuned Β· Off-Brand Β· Sharing is Caring Β· Field Notes
|
| 653 |
"""
|
| 654 |
)
|
| 655 |
|
data/SOURCES.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
|
| 3 |
The Apiarist model needs photos of **honeycomb frames** (the wooden rectangles
|
| 4 |
bees build comb on), not just bees flying around. iNaturalist gives us a base
|
| 5 |
-
of bee imagery but is light on hive-frame shots
|
| 6 |
sources below.
|
| 7 |
|
| 8 |
## 1. iNaturalist (auto-scraped)
|
|
@@ -17,7 +17,7 @@ Output: `data/raw/inaturalist/` + `metadata.jsonl`. Resumable.
|
|
| 17 |
|
| 18 |
## 2. Roboflow Universe (hand-search, often pre-labeled)
|
| 19 |
|
| 20 |
-
Search these queries
|
| 21 |
|
| 22 |
- https://universe.roboflow.com/search?q=honeycomb
|
| 23 |
- https://universe.roboflow.com/search?q=varroa+mite
|
|
@@ -66,8 +66,8 @@ Suggested channels (manual licensing review required):
|
|
| 66 |
## Labeling
|
| 67 |
|
| 68 |
Use Roboflow's free tier (up to 1k images). Define 3 classes:
|
| 69 |
-
- `queen`
|
| 70 |
-
- `mite`
|
| 71 |
-
- `swarm_cell`
|
| 72 |
|
| 73 |
Export as YOLOv8 format β drop into `data/processed/yolo/`.
|
|
|
|
| 2 |
|
| 3 |
The Apiarist model needs photos of **honeycomb frames** (the wooden rectangles
|
| 4 |
bees build comb on), not just bees flying around. iNaturalist gives us a base
|
| 5 |
+
of bee imagery but is light on hive-frame shots, we supplement from other
|
| 6 |
sources below.
|
| 7 |
|
| 8 |
## 1. iNaturalist (auto-scraped)
|
|
|
|
| 17 |
|
| 18 |
## 2. Roboflow Universe (hand-search, often pre-labeled)
|
| 19 |
|
| 20 |
+
Search these queries, many datasets ship with YOLO labels already.
|
| 21 |
|
| 22 |
- https://universe.roboflow.com/search?q=honeycomb
|
| 23 |
- https://universe.roboflow.com/search?q=varroa+mite
|
|
|
|
| 66 |
## Labeling
|
| 67 |
|
| 68 |
Use Roboflow's free tier (up to 1k images). Define 3 classes:
|
| 69 |
+
- `queen`, bounding box around the queen
|
| 70 |
+
- `mite`, small box per visible mite
|
| 71 |
+
- `swarm_cell`, box around each peanut-shaped cell
|
| 72 |
|
| 73 |
Export as YOLOv8 format β drop into `data/processed/yolo/`.
|
db.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
SQLite layer for hive registry + inspection history.
|
| 3 |
|
| 4 |
Storage is ephemeral on HF Spaces' free tier (container restart wipes
|
| 5 |
-
the filesystem)
|
| 6 |
deployment we'd back this with HF Hub persistence or a real DB.
|
| 7 |
"""
|
| 8 |
|
|
|
|
| 2 |
SQLite layer for hive registry + inspection history.
|
| 3 |
|
| 4 |
Storage is ephemeral on HF Spaces' free tier (container restart wipes
|
| 5 |
+
the filesystem), that's fine for a hackathon demo. For real-world
|
| 6 |
deployment we'd back this with HF Hub persistence or a real DB.
|
| 7 |
"""
|
| 8 |
|
detector.py
CHANGED
|
@@ -3,7 +3,7 @@ YOLOv8 specialist detector for bees / drones / queens / pollen bees.
|
|
| 3 |
|
| 4 |
Loads the custom-trained weights from weights/honey_bee_detector.pt if
|
| 5 |
present. Degrades gracefully (no-op detections, original image) if the
|
| 6 |
-
weights file is missing
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
|
@@ -88,7 +88,7 @@ def _try_load_yolo():
|
|
| 88 |
try:
|
| 89 |
from ultralytics import YOLO
|
| 90 |
_yolo = YOLO(str(WEIGHTS_PATH))
|
| 91 |
-
# Force CPU
|
| 92 |
# ZeroGPU's CUDA emulation rejects torch.cuda calls outside @gpu.
|
| 93 |
_yolo.to("cpu")
|
| 94 |
print(f"[detector] loaded YOLO weights from {WEIGHTS_PATH} (cpu)",
|
|
@@ -122,11 +122,11 @@ _CLASS_ALIASES = {
|
|
| 122 |
|
| 123 |
|
| 124 |
_PER_CLASS_CONF = {
|
| 125 |
-
"bee":
|
| 126 |
-
"drone":
|
| 127 |
-
"varroa":
|
| 128 |
"pollenbee": 0.30,
|
| 129 |
-
"queen":
|
| 130 |
}
|
| 131 |
|
| 132 |
|
|
@@ -177,11 +177,11 @@ def detect(
|
|
| 177 |
# Per-class drawing config. RGB(A). Tuned for clarity over a busy
|
| 178 |
# honeycomb background.
|
| 179 |
_CLASS_STYLES = {
|
| 180 |
-
"bee":
|
| 181 |
-
"drone":
|
| 182 |
"pollenbee": {"color": (255, 220, 80), "width": 1, "label": None},
|
| 183 |
-
"varroa":
|
| 184 |
-
"queen":
|
| 185 |
}
|
| 186 |
|
| 187 |
|
|
|
|
| 3 |
|
| 4 |
Loads the custom-trained weights from weights/honey_bee_detector.pt if
|
| 5 |
present. Degrades gracefully (no-op detections, original image) if the
|
| 6 |
+
weights file is missing, useful while training is still in progress.
|
| 7 |
"""
|
| 8 |
|
| 9 |
from __future__ import annotations
|
|
|
|
| 88 |
try:
|
| 89 |
from ultralytics import YOLO
|
| 90 |
_yolo = YOLO(str(WEIGHTS_PATH))
|
| 91 |
+
# Force CPU, we run on the main container, not the ZeroGPU worker.
|
| 92 |
# ZeroGPU's CUDA emulation rejects torch.cuda calls outside @gpu.
|
| 93 |
_yolo.to("cpu")
|
| 94 |
print(f"[detector] loaded YOLO weights from {WEIGHTS_PATH} (cpu)",
|
|
|
|
| 122 |
|
| 123 |
|
| 124 |
_PER_CLASS_CONF = {
|
| 125 |
+
"bee": 0.25,
|
| 126 |
+
"drone": 0.55, # higher, drones false-positive on dark shapes (fingers, shadows)
|
| 127 |
+
"varroa": 0.30, # mites are tiny; moderate threshold
|
| 128 |
"pollenbee": 0.30,
|
| 129 |
+
"queen": 0.15, # lower, try harder to find her even at lower confidence
|
| 130 |
}
|
| 131 |
|
| 132 |
|
|
|
|
| 177 |
# Per-class drawing config. RGB(A). Tuned for clarity over a busy
|
| 178 |
# honeycomb background.
|
| 179 |
_CLASS_STYLES = {
|
| 180 |
+
"bee": {"color": (244, 163, 0), "width": 1, "label": None},
|
| 181 |
+
"drone": {"color": (255, 80, 80), "width": 1, "label": None},
|
| 182 |
"pollenbee": {"color": (255, 220, 80), "width": 1, "label": None},
|
| 183 |
+
"varroa": {"color": (220, 50, 220), "width": 2, "label": "mite"},
|
| 184 |
+
"queen": {"color": (50, 255, 100), "width": 4, "label": "QUEEN"},
|
| 185 |
}
|
| 186 |
|
| 187 |
|
report.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
PDF report generator for an apiary's weekly inspections.
|
| 3 |
|
| 4 |
Pulls inspection rows from SQLite (db.py) and renders a clean one-page
|
| 5 |
-
report per hive plus a summary page. Uses reportlab
|
| 6 |
system deps, ships fine on HF Spaces.
|
| 7 |
"""
|
| 8 |
|
|
@@ -62,7 +62,7 @@ def _styles():
|
|
| 62 |
|
| 63 |
def _date(epoch: float | None) -> str:
|
| 64 |
if not epoch:
|
| 65 |
-
return "
|
| 66 |
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
|
| 67 |
|
| 68 |
|
|
@@ -75,7 +75,7 @@ def _hive_table(inspections: list[dict]) -> Table:
|
|
| 75 |
"Y" if i["queen_detected"] else "N",
|
| 76 |
str(i["varroa_mites_visible"] or 0),
|
| 77 |
"Y" if i["swarm_cells_detected"] else "N",
|
| 78 |
-
i["frame_health"] or "
|
| 79 |
(i["notes"] or "")[:80],
|
| 80 |
]
|
| 81 |
)
|
|
@@ -115,7 +115,7 @@ def generate_report() -> bytes:
|
|
| 115 |
)
|
| 116 |
|
| 117 |
story = []
|
| 118 |
-
story.append(Paragraph("
|
| 119 |
story.append(
|
| 120 |
Paragraph(
|
| 121 |
f"Generated {time.strftime('%Y-%m-%d %H:%M')}",
|
|
|
|
| 2 |
PDF report generator for an apiary's weekly inspections.
|
| 3 |
|
| 4 |
Pulls inspection rows from SQLite (db.py) and renders a clean one-page
|
| 5 |
+
report per hive plus a summary page. Uses reportlab, pure Python, no
|
| 6 |
system deps, ships fine on HF Spaces.
|
| 7 |
"""
|
| 8 |
|
|
|
|
| 62 |
|
| 63 |
def _date(epoch: float | None) -> str:
|
| 64 |
if not epoch:
|
| 65 |
+
return "-"
|
| 66 |
return time.strftime("%Y-%m-%d %H:%M", time.localtime(epoch))
|
| 67 |
|
| 68 |
|
|
|
|
| 75 |
"Y" if i["queen_detected"] else "N",
|
| 76 |
str(i["varroa_mites_visible"] or 0),
|
| 77 |
"Y" if i["swarm_cells_detected"] else "N",
|
| 78 |
+
i["frame_health"] or "-",
|
| 79 |
(i["notes"] or "")[:80],
|
| 80 |
]
|
| 81 |
)
|
|
|
|
| 115 |
)
|
| 116 |
|
| 117 |
story = []
|
| 118 |
+
story.append(Paragraph(" Apiarist Inspection Report", styles["ApiaristTitle"]))
|
| 119 |
story.append(
|
| 120 |
Paragraph(
|
| 121 |
f"Generated {time.strftime('%Y-%m-%d %H:%M')}",
|
scripts/download_yolo_weights.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Pull Matt Nudi's honey-bee/drone/queen YOLO weights from Roboflow once,
|
| 3 |
then copy the .pt file into weights/ so the Space can load it locally
|
| 4 |
-
with ultralytics
|
| 5 |
|
| 6 |
Usage:
|
| 7 |
py scripts/download_yolo_weights.py
|
|
@@ -70,9 +70,9 @@ def main() -> None:
|
|
| 70 |
# Pick the most recently modified
|
| 71 |
latest = max(pt_files, key=lambda p: p.stat().st_mtime)
|
| 72 |
shutil.copy(latest, TARGET_PT)
|
| 73 |
-
print(f"\n
|
| 74 |
-
print(f"
|
| 75 |
-
print(f"
|
| 76 |
return
|
| 77 |
print("inference cache had no .pt files yet; falling back to dataset export.")
|
| 78 |
except Exception as e:
|
|
@@ -86,8 +86,8 @@ def main() -> None:
|
|
| 86 |
pt_files = list(Path(dataset.location).rglob("*.pt"))
|
| 87 |
if pt_files:
|
| 88 |
shutil.copy(pt_files[0], TARGET_PT)
|
| 89 |
-
print(f"\n
|
| 90 |
-
print(f"
|
| 91 |
return
|
| 92 |
|
| 93 |
raise SystemExit(
|
|
|
|
| 1 |
"""
|
| 2 |
Pull Matt Nudi's honey-bee/drone/queen YOLO weights from Roboflow once,
|
| 3 |
then copy the .pt file into weights/ so the Space can load it locally
|
| 4 |
+
with ultralytics, no Roboflow auth needed at runtime.
|
| 5 |
|
| 6 |
Usage:
|
| 7 |
py scripts/download_yolo_weights.py
|
|
|
|
| 70 |
# Pick the most recently modified
|
| 71 |
latest = max(pt_files, key=lambda p: p.stat().st_mtime)
|
| 72 |
shutil.copy(latest, TARGET_PT)
|
| 73 |
+
print(f"\n Copied weights from {latest}")
|
| 74 |
+
print(f" to {TARGET_PT.resolve()}")
|
| 75 |
+
print(f" size: {TARGET_PT.stat().st_size / 1024 / 1024:.1f} MB")
|
| 76 |
return
|
| 77 |
print("inference cache had no .pt files yet; falling back to dataset export.")
|
| 78 |
except Exception as e:
|
|
|
|
| 86 |
pt_files = list(Path(dataset.location).rglob("*.pt"))
|
| 87 |
if pt_files:
|
| 88 |
shutil.copy(pt_files[0], TARGET_PT)
|
| 89 |
+
print(f"\n Copied weights to {TARGET_PT.resolve()}")
|
| 90 |
+
print(f" size: {TARGET_PT.stat().st_size / 1024 / 1024:.1f} MB")
|
| 91 |
return
|
| 92 |
|
| 93 |
raise SystemExit(
|
scripts/extract_dataset.py
CHANGED
|
@@ -41,7 +41,7 @@ def main() -> None:
|
|
| 41 |
fail = 0
|
| 42 |
for i, member in enumerate(members):
|
| 43 |
if i % 500 == 0:
|
| 44 |
-
print(f"
|
| 45 |
target = DEST / member
|
| 46 |
try:
|
| 47 |
target.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -58,7 +58,7 @@ def main() -> None:
|
|
| 58 |
except Exception as e:
|
| 59 |
fail += 1
|
| 60 |
if fail <= 5:
|
| 61 |
-
print(f"
|
| 62 |
|
| 63 |
print(f"\nDone. ok={ok}, fail={fail}")
|
| 64 |
|
|
|
|
| 41 |
fail = 0
|
| 42 |
for i, member in enumerate(members):
|
| 43 |
if i % 500 == 0:
|
| 44 |
+
print(f" progress: {i}/{total} (ok={ok}, fail={fail})")
|
| 45 |
target = DEST / member
|
| 46 |
try:
|
| 47 |
target.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
| 58 |
except Exception as e:
|
| 59 |
fail += 1
|
| 60 |
if fail <= 5:
|
| 61 |
+
print(f" [!] {member[:80]}... -> {type(e).__name__}: {e}")
|
| 62 |
|
| 63 |
print(f"\nDone. ok={ok}, fail={fail}")
|
| 64 |
|
scripts/push_dataset_to_hub.py
CHANGED
|
@@ -35,7 +35,7 @@ size_categories:
|
|
| 35 |
# Apiarist iNaturalist bee photos
|
| 36 |
|
| 37 |
Honey-bee (*Apis mellifera*) photos scraped from the iNaturalist API as
|
| 38 |
-
training context for **Apiarist**
|
| 39 |
inspector built for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 40 |
|
| 41 |
## Composition
|
|
@@ -44,8 +44,8 @@ inspector built for the [Build Small Hackathon](https://huggingface.co/build-sma
|
|
| 44 |
- Most are forager bees on flowers; useful as bee context for VLM prompting
|
| 45 |
|
| 46 |
## Files
|
| 47 |
-
- `images/`
|
| 48 |
-
- `metadata.jsonl`
|
| 49 |
|
| 50 |
## License
|
| 51 |
Each photo retains its original Creative Commons license (see `metadata.jsonl`).
|
|
@@ -116,7 +116,7 @@ def main() -> None:
|
|
| 116 |
)
|
| 117 |
|
| 118 |
print(
|
| 119 |
-
f"\n
|
| 120 |
)
|
| 121 |
|
| 122 |
|
|
|
|
| 35 |
# Apiarist iNaturalist bee photos
|
| 36 |
|
| 37 |
Honey-bee (*Apis mellifera*) photos scraped from the iNaturalist API as
|
| 38 |
+
training context for **Apiarist**, a fully-offline AI hive frame
|
| 39 |
inspector built for the [Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 40 |
|
| 41 |
## Composition
|
|
|
|
| 44 |
- Most are forager bees on flowers; useful as bee context for VLM prompting
|
| 45 |
|
| 46 |
## Files
|
| 47 |
+
- `images/`, JPEG photos, filenames are iNaturalist photo IDs
|
| 48 |
+
- `metadata.jsonl`, one line per photo: license, observation ID, observed date, place, source URL
|
| 49 |
|
| 50 |
## License
|
| 51 |
Each photo retains its original Creative Commons license (see `metadata.jsonl`).
|
|
|
|
| 116 |
)
|
| 117 |
|
| 118 |
print(
|
| 119 |
+
f"\n Dataset live at: https://huggingface.co/datasets/{args.repo_id}"
|
| 120 |
)
|
| 121 |
|
| 122 |
|
scripts/push_yolo_to_hub.py
CHANGED
|
@@ -37,7 +37,7 @@ pipeline_tag: object-detection
|
|
| 37 |
# Apiarist Honey-Bee Detector (YOLOv8s)
|
| 38 |
|
| 39 |
A custom-trained **YOLOv8s** specialist detector for honeycomb frame
|
| 40 |
-
inspection. Built as part of [Apiarist](https://huggingface.co/spaces/build-small-hackathon/Apiarist)
|
| 41 |
backyard beekeepers, made for the
|
| 42 |
[Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 43 |
|
|
@@ -46,10 +46,10 @@ backyard beekeepers, made for the
|
|
| 46 |
Trained on the [hendricks_ricky bee-project](https://universe.roboflow.com/hendricks_ricky-hotmail-de/bee-project) dataset
|
| 47 |
(3,308 labeled images, including 892 queen-bee examples). Four classes:
|
| 48 |
|
| 49 |
-
- `Queen`
|
| 50 |
-
- `Worker`
|
| 51 |
-
- `Drone`
|
| 52 |
-
- `Varroa`
|
| 53 |
|
| 54 |
## Usage
|
| 55 |
|
|
@@ -71,9 +71,9 @@ frame photos):
|
|
| 71 |
```python
|
| 72 |
PER_CLASS_CONF = {
|
| 73 |
"Worker": 0.25,
|
| 74 |
-
"Drone":
|
| 75 |
-
"Varroa": 0.30,
|
| 76 |
-
"Queen":
|
| 77 |
}
|
| 78 |
```
|
| 79 |
|
|
@@ -91,7 +91,7 @@ PER_CLASS_CONF = {
|
|
| 91 |
The training set includes ~892 queen images, which is far more than the
|
| 92 |
typical bee-detection dataset but still limited compared to the
|
| 93 |
millions of bees in worker class. Queen detection precision is
|
| 94 |
-
moderate
|
| 95 |
**candidate flag**, not as ground truth.
|
| 96 |
|
| 97 |
Varroa mite detection works on close-up frames where mites are visible
|
|
|
|
| 37 |
# Apiarist Honey-Bee Detector (YOLOv8s)
|
| 38 |
|
| 39 |
A custom-trained **YOLOv8s** specialist detector for honeycomb frame
|
| 40 |
+
inspection. Built as part of [Apiarist](https://huggingface.co/spaces/build-small-hackathon/Apiarist), a fully-offline AI hive inspector for
|
| 41 |
backyard beekeepers, made for the
|
| 42 |
[Build Small Hackathon](https://huggingface.co/build-small-hackathon).
|
| 43 |
|
|
|
|
| 46 |
Trained on the [hendricks_ricky bee-project](https://universe.roboflow.com/hendricks_ricky-hotmail-de/bee-project) dataset
|
| 47 |
(3,308 labeled images, including 892 queen-bee examples). Four classes:
|
| 48 |
|
| 49 |
+
- `Queen`, queen bee (larger, elongated abdomen)
|
| 50 |
+
- `Worker`, worker / forager bees (the majority class)
|
| 51 |
+
- `Drone`, male / drone bees
|
| 52 |
+
- `Varroa`, varroa destructor mites visible on bees or comb
|
| 53 |
|
| 54 |
## Usage
|
| 55 |
|
|
|
|
| 71 |
```python
|
| 72 |
PER_CLASS_CONF = {
|
| 73 |
"Worker": 0.25,
|
| 74 |
+
"Drone": 0.55, # higher, drones false-positive on fingers, shadows
|
| 75 |
+
"Varroa": 0.30, # mites are small
|
| 76 |
+
"Queen": 0.15, # lower, try harder to find her
|
| 77 |
}
|
| 78 |
```
|
| 79 |
|
|
|
|
| 91 |
The training set includes ~892 queen images, which is far more than the
|
| 92 |
typical bee-detection dataset but still limited compared to the
|
| 93 |
millions of bees in worker class. Queen detection precision is
|
| 94 |
+
moderate, when the model labels a queen, verify visually. Use as a
|
| 95 |
**candidate flag**, not as ground truth.
|
| 96 |
|
| 97 |
Varroa mite detection works on close-up frames where mites are visible
|
scripts/scrape_inaturalist.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
Scrape Apis mellifera (Western honeybee) photos from the iNaturalist API.
|
| 3 |
|
| 4 |
-
Most observations are forager bees on flowers, not hive frames
|
| 5 |
still give Moondream2 plenty of bee imagery to ground its responses, and
|
| 6 |
the high-quality bee close-ups are useful negatives / context for YOLO.
|
| 7 |
|
|
@@ -102,12 +102,12 @@ def main() -> None:
|
|
| 102 |
try:
|
| 103 |
data = search_page(args.taxon, page, args.per_page)
|
| 104 |
except Exception as e:
|
| 105 |
-
print(f"
|
| 106 |
time.sleep(2)
|
| 107 |
continue
|
| 108 |
|
| 109 |
obs_list = data.get("results", [])
|
| 110 |
-
print(f"
|
| 111 |
|
| 112 |
for obs in tqdm(obs_list, desc=f"page {page}", leave=False):
|
| 113 |
for photo in obs.get("photos", []):
|
|
@@ -125,7 +125,7 @@ def main() -> None:
|
|
| 125 |
try:
|
| 126 |
download_photo(url, dest)
|
| 127 |
except Exception as e:
|
| 128 |
-
tqdm.write(f"
|
| 129 |
continue
|
| 130 |
|
| 131 |
rec = {
|
|
@@ -142,11 +142,11 @@ def main() -> None:
|
|
| 142 |
seen.add(photo_id)
|
| 143 |
total_new += 1
|
| 144 |
|
| 145 |
-
# iNaturalist asks for max 1 req/sec
|
| 146 |
time.sleep(1.5)
|
| 147 |
|
| 148 |
-
print(f"\n
|
| 149 |
-
print(f"
|
| 150 |
|
| 151 |
|
| 152 |
if __name__ == "__main__":
|
|
|
|
| 1 |
"""
|
| 2 |
Scrape Apis mellifera (Western honeybee) photos from the iNaturalist API.
|
| 3 |
|
| 4 |
+
Most observations are forager bees on flowers, not hive frames, but they
|
| 5 |
still give Moondream2 plenty of bee imagery to ground its responses, and
|
| 6 |
the high-quality bee close-ups are useful negatives / context for YOLO.
|
| 7 |
|
|
|
|
| 102 |
try:
|
| 103 |
data = search_page(args.taxon, page, args.per_page)
|
| 104 |
except Exception as e:
|
| 105 |
+
print(f" [!] page {page} failed: {e}")
|
| 106 |
time.sleep(2)
|
| 107 |
continue
|
| 108 |
|
| 109 |
obs_list = data.get("results", [])
|
| 110 |
+
print(f" {len(obs_list)} observations returned")
|
| 111 |
|
| 112 |
for obs in tqdm(obs_list, desc=f"page {page}", leave=False):
|
| 113 |
for photo in obs.get("photos", []):
|
|
|
|
| 125 |
try:
|
| 126 |
download_photo(url, dest)
|
| 127 |
except Exception as e:
|
| 128 |
+
tqdm.write(f" [!] photo {photo_id} failed: {e}")
|
| 129 |
continue
|
| 130 |
|
| 131 |
rec = {
|
|
|
|
| 142 |
seen.add(photo_id)
|
| 143 |
total_new += 1
|
| 144 |
|
| 145 |
+
# iNaturalist asks for max 1 req/sec, be polite
|
| 146 |
time.sleep(1.5)
|
| 147 |
|
| 148 |
+
print(f"\n Done. Downloaded {total_new} new photos this run.")
|
| 149 |
+
print(f" Collection total: {len(seen)} photos in {output_dir}")
|
| 150 |
|
| 151 |
|
| 152 |
if __name__ == "__main__":
|
scripts/train_yolo_on_modal.py
CHANGED
|
@@ -119,6 +119,6 @@ def main() -> None:
|
|
| 119 |
print("=" * 60)
|
| 120 |
print(
|
| 121 |
"\nDownload locally with:\n"
|
| 122 |
-
f"
|
| 123 |
f"weights/honey_bee_detector.pt"
|
| 124 |
)
|
|
|
|
| 119 |
print("=" * 60)
|
| 120 |
print(
|
| 121 |
"\nDownload locally with:\n"
|
| 122 |
+
f" modal volume get {VOLUME_NAME} /apiarist/weights/best.pt "
|
| 123 |
f"weights/honey_bee_detector.pt"
|
| 124 |
)
|