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 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 β€” 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,7 +14,7 @@ Live demo: [huggingface.co/spaces/build-small-hackathon/Apiarist](https://huggin
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,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 β€” 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,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 β€” 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
 
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 β€” 60s Demo Video Script
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–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,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 β€” 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
 
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** β€” 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,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) β€” 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)
 
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
- # 🐝 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,15 +50,15 @@ All local. No cloud APIs at inference.
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
 
 
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 β€” no-op outside HF Spaces
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
- "βœ… 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,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" β€’ **{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,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"πŸ”Ž ~{r['varroa_mites_visible']} mite(s) (VLM estimate, not specialist)"
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']} | **Overall:** {r['frame_health']}
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("🐝 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,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("πŸ€– 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,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']} **Health:** {r['frame_health']}
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
- "⚠️ 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,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 # 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,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("# 🐝 APIARIST")
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("πŸ” Inspect"):
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
- "πŸ”¬ Analyze Frame", variant="primary"
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("πŸ“‹ Hives") as hives_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("βž• Add Hive", variant="primary")
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("πŸ”„ Refresh")
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
- "πŸ“„ 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,33 +587,33 @@ def build_ui() -> gr.Blocks:
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,9 +623,9 @@ anywhere.
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,10 +633,10 @@ internet required.
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,7 +649,7 @@ internet required.
649
 
650
  ### Badges chased
651
 
652
- πŸ”Œ Off the Grid Β· 🎯 Well-Tuned Β· 🎨 Off-Brand Β· πŸ“‘ Sharing is Caring Β· πŸ““ Field Notes
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 β€” we supplement from other
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 β€” 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,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` β€” 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/`.
 
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) β€” 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
 
 
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 β€” useful while training is still in progress.
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 β€” 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,11 +122,11 @@ _CLASS_ALIASES = {
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,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": {"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
 
 
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 β€” pure Python, no
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("🐝 Apiarist Inspection Report", styles["ApiaristTitle"]))
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 β€” no Roboflow auth needed at runtime.
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βœ“ 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,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βœ“ 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(
 
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" progress: {i}/{total} (ok={ok}, fail={fail})")
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" [!] {member[:80]}... -> {type(e).__name__}: {e}")
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** β€” a fully-offline AI hive frame
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/` β€” 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,7 +116,7 @@ def main() -> None:
116
  )
117
 
118
  print(
119
- f"\nβœ“ Dataset live at: https://huggingface.co/datasets/{args.repo_id}"
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) β€” 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,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` β€” 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,9 +71,9 @@ frame photos):
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,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 β€” 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
 
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 β€” 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,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" [!] 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,7 +125,7 @@ def main() -> None:
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,11 +142,11 @@ def main() -> None:
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__":
 
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" modal volume get {VOLUME_NAME} /apiarist/weights/best.pt "
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
  )