Pre-share fixes from final review pass
Blockers - http://birdnet.local/avian/ now serves the collage (added avian/index.html redirect to frontend/; previously the symlink target had no top-level index, so users saw a directory listing of api/ assets/ forwarding/ frontend/ scripts/). - Gemini model bumped from gemini-2.5-flash-image-preview (deprecated 2026-01-15) to the stable gemini-2.5-flash-image. Pregen was non- functional out of the box. - avian-mqtt.service: dropped %h (which expands to /root for system-mode units regardless of User=). Use WorkingDirectory=~ which DOES respect User=, plus a relative ExecStart. - birdnet-api.php case 'lifelist' now aliases COUNT(*) AS n (was 'total'), matching what apt.js reads for atlas card counts. Atlas previously always displayed 0. Bugs - Retry-After parsing handles HTTP-date format without crashing. - apt.js no longer fetches firstseen/timeseries (declared in state but never consumed). Two wasted round-trips per page load removed. - update_birdnet.sh defaults branch=avian-visitors so the (opt-in) Sunday auto-update cron doesn't reset HEAD to upstream main and delete the avian/ overlay. - recording.php: removed dead $BIRDS_JSON_CANDIDATES. Polish - Pregen --sleep default raised 4→6s (more headroom under free-tier RPM). - en-dash in README BOM (~$35–80) replaced with hyphen for style match. - Slop sweep across user-facing docs: no em-dashes, no en-dashes, no AI-slop word hits (robust/comprehensive/seamless/etc.).
This commit is contained in:
+17
-15
@@ -1,30 +1,30 @@
|
||||
<?php
|
||||
// AvianVisitors — JSON facade over BirdNET-Pi's birds.db. Read-only.
|
||||
// AvianVisitors - JSON facade over BirdNET-Pi's birds.db. Read-only.
|
||||
// Symlinked into the BirdNET-Pi Caddy site root at /avian/api/.
|
||||
//
|
||||
// Endpoints (?action=...):
|
||||
// stats — totals (detections, unique species, today, last hour)
|
||||
// lifelist — every species with first_seen, last_seen, total_count
|
||||
// recent — &hours=N (default 24): species heard in the window
|
||||
// species — &sci=<sci_name>: per-species detail page
|
||||
// timeseries — &days=N: daily detection counts per species
|
||||
// firstseen — every species' earliest detection
|
||||
// stats - totals (detections, unique species, today, last hour)
|
||||
// lifelist - every species with first_seen, last_seen, total_count
|
||||
// recent - &hours=N (default 24): species heard in the window
|
||||
// species - &sci=<sci_name>: per-species detail page
|
||||
// timeseries - &days=N: daily detection counts per species
|
||||
// firstseen - every species' earliest detection
|
||||
//
|
||||
// Default LAN deploy ships without auth. If you've exposed the Pi via
|
||||
// Cloudflare or a tunnel, add a Caddy `basic_auth` matcher around the
|
||||
// /avian/api/* path — see avian/forwarding/.
|
||||
// /avian/api/* path - see avian/forwarding/.
|
||||
|
||||
declare(strict_types=1);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
header('Cache-Control: public, max-age=30');
|
||||
|
||||
// SCRIPT_FILENAME on the Pi resolves through the symlink to
|
||||
// $HOME/BirdNET-Pi/avian/api/birdnet-api.php — walk three dirs up to
|
||||
// $HOME/BirdNET-Pi/avian/api/birdnet-api.php - walk three dirs up to
|
||||
// reach the install root, then point at scripts/birds.db. Lets a Pi
|
||||
// installed under any username (the BirdNET-Pi installer uses $USER,
|
||||
// not a fixed name) work without editing this file.
|
||||
$DB_PATH = dirname(__DIR__, 3) . '/scripts/birds.db';
|
||||
// Fallback if the symlink layout ever changes — keeps the most common
|
||||
// Fallback if the symlink layout ever changes - keeps the most common
|
||||
// install path working even if SCRIPT_FILENAME oddities trip __DIR__.
|
||||
if (!file_exists($DB_PATH)) {
|
||||
$alt = getenv('HOME') . '/BirdNET-Pi/scripts/birds.db';
|
||||
@@ -84,9 +84,11 @@ switch ($action) {
|
||||
}
|
||||
|
||||
case 'lifelist': {
|
||||
// n = total calls (matches the `recent` action's alias so the
|
||||
// frontend can read either response interchangeably).
|
||||
$rs = rows($db,
|
||||
"SELECT Sci_Name AS sci, Com_Name AS com, MIN(Date||' '||Time) AS first_seen, "
|
||||
. " MAX(Date||' '||Time) AS last_seen, COUNT(*) AS total, MAX(Confidence) AS best_conf "
|
||||
. " MAX(Date||' '||Time) AS last_seen, COUNT(*) AS n, MAX(Confidence) AS best_conf "
|
||||
. "FROM detections GROUP BY Sci_Name ORDER BY first_seen ASC"
|
||||
);
|
||||
echo json_encode(['species' => $rs, 'as_of' => date('c')]);
|
||||
@@ -145,9 +147,9 @@ switch ($action) {
|
||||
|
||||
case 'timeseries': {
|
||||
// Aggregated time-bucketed counts for the stats charts.
|
||||
// daily — last $days days, detections + unique species per day
|
||||
// by_hour — detections grouped by hour of day, last 30 days
|
||||
// The frontend backfills missing dates with zero — sparse data days
|
||||
// daily - last $days days, detections + unique species per day
|
||||
// by_hour - detections grouped by hour of day, last 30 days
|
||||
// The frontend backfills missing dates with zero - sparse data days
|
||||
// are otherwise dropped by the GROUP BY.
|
||||
$days = max(1, min(90, (int)($_GET['days'] ?? 30)));
|
||||
$daily = rows($db,
|
||||
@@ -172,7 +174,7 @@ switch ($action) {
|
||||
}
|
||||
|
||||
case 'firstseen': {
|
||||
// Most recent additions to the life list — first detection per
|
||||
// Most recent additions to the life list - first detection per
|
||||
// species, sorted by first_seen DESC. Powers the "First Detections"
|
||||
// section on the stats view.
|
||||
$limit = max(1, min(50, (int)($_GET['limit'] ?? 10)));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// AvianVisitors — bird image resolver.
|
||||
// AvianVisitors - bird image resolver.
|
||||
//
|
||||
// Lookup chain for /avian/api/cutout.php?sci=Calypte+anna:
|
||||
// 1. ../assets/illustrations/<slug>.png (450+ bundled kachō-e renders)
|
||||
@@ -7,11 +7,11 @@
|
||||
// 3. cached rembg of a Wikipedia photo at $HOME/BirdSongs/Extracted/cutouts/
|
||||
// 4. fresh Wikipedia → rembg → cache (skipped gracefully if rembg unset)
|
||||
//
|
||||
// The frontend's <img src> points here for every species — bundled
|
||||
// The frontend's <img src> points here for every species - bundled
|
||||
// hits return instantly; cold misses fall through to the dynamic path.
|
||||
//
|
||||
// Default LAN deploy ships without auth. To expose publicly, gate
|
||||
// /avian/api/* with basic_auth in your Caddyfile — see avian/forwarding/.
|
||||
// /avian/api/* with basic_auth in your Caddyfile - see avian/forwarding/.
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -61,7 +61,7 @@ if (is_file($cachePath) && filesize($cachePath) > 1024) {
|
||||
}
|
||||
|
||||
// 4. Fresh Wikipedia fetch + rembg. Skipped if rembg-cli isn't on
|
||||
// PATH — the resolver simply returns a 404 in that case rather
|
||||
// PATH - the resolver simply returns a 404 in that case rather
|
||||
// than burning a Wikipedia request we can't use.
|
||||
$rembg = '/usr/local/bin/rembg-cli';
|
||||
if (!is_executable($rembg)) {
|
||||
@@ -107,7 +107,7 @@ if (!$imgBytes || strlen($imgBytes) < 1024) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// rembg via the wrapper. u2netp = lightweight model (~50MB peak RAM —
|
||||
// rembg via the wrapper. u2netp = lightweight model (~50MB peak RAM -
|
||||
// matters on the Pi 3B+). Temp files because rembg's CLI prefers
|
||||
// real paths.
|
||||
$tmpInBase = tempnam(sys_get_temp_dir(), 'rembg-in-');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
// AvianVisitors — serves the most-recent detection mp3 for a given
|
||||
// AvianVisitors - serves the most-recent detection mp3 for a given
|
||||
// scientific name. Called by the collage detail modal at
|
||||
// /avian/api/recording.php?sci=<name>.
|
||||
//
|
||||
@@ -24,7 +24,7 @@ if ($sci === '' && $file === '') {
|
||||
|
||||
// Reject any sci-name that isn't a clean Genus species[ subspecies[ tri]]
|
||||
// pattern. resolve_common() below falls back to str_replace(' ', '_', $sci)
|
||||
// when there's no birds.json match — without this guard, `?sci=../etc` would
|
||||
// when there's no birds.json match - without this guard, `?sci=../etc` would
|
||||
// flow through unmodified into a filesystem path.
|
||||
if ($sci !== '' && !preg_match('/^[A-Za-z]{2,40}(?:[ ][a-z]{2,40}){1,3}$/', $sci)) {
|
||||
http_response_code(400);
|
||||
@@ -93,14 +93,9 @@ if ($file !== '') {
|
||||
readfile($path);
|
||||
exit;
|
||||
}
|
||||
$BIRDS_JSON_CANDIDATES = [
|
||||
getenv('HOME') . '/BirdNET-Pi/scripts/birds.json',
|
||||
getenv('HOME') . '/BirdNET-Pi/model/labels.txt',
|
||||
];
|
||||
|
||||
// ---- Resolve scientific name → common name (with underscores) ----
|
||||
function resolve_common(string $sci): ?string {
|
||||
// Try birds.json first (preferred — has clean sci/com pairs).
|
||||
// Try birds.json first (preferred - has clean sci/com pairs).
|
||||
foreach ([getenv('HOME') . '/BirdNET-Pi/scripts/birds.json'] as $f) {
|
||||
if (is_readable($f)) {
|
||||
$list = json_decode((string)file_get_contents($f), true);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
// AvianVisitors — serves the spectrogram PNG that BirdNET-Pi generates
|
||||
// AvianVisitors - serves the spectrogram PNG that BirdNET-Pi generates
|
||||
// alongside each detection mp3. Same lookup logic as recording.php (find
|
||||
// the matching file under By_Date/<date>/<Common_Name>/) — just .png
|
||||
// the matching file under By_Date/<date>/<Common_Name>/) - just .png
|
||||
// instead of .mp3.
|
||||
//
|
||||
// Endpoints:
|
||||
@@ -10,7 +10,7 @@
|
||||
// recording (atlas modal uses this so the
|
||||
// strip below each play button is the
|
||||
// spectrogram for that recording, not
|
||||
// "the most recent" — they can differ).
|
||||
// "the most recent" - they can differ).
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
@@ -44,7 +44,7 @@ if ($file !== '') {
|
||||
exit;
|
||||
}
|
||||
// BirdNET-Pi names the spectrogram as the FULL mp3 filename plus
|
||||
// ".png" — e.g. "American_Crow-82-…-20:25:29.mp3" pairs with
|
||||
// ".png" - e.g. "American_Crow-82-…-20:25:29.mp3" pairs with
|
||||
// "American_Crow-82-…-20:25:29.mp3.png" (not "…-20:25:29.png").
|
||||
// Accept either form gracefully.
|
||||
if (substr($file, -4) === '.png') {
|
||||
|
||||
Reference in New Issue
Block a user