AvianVisitors: BirdNET-Pi collage frontend overlay

Adds avian/ on top of upstream BirdNET-Pi:
- frontend/    static collage UI (mask-packed bird tiles, sized by detection count)
- assets/      ~450 bundled kachō-e illustrations + cutouts + binary masks
- api/         PHP shims for recent/lifelist/firstseen/timeseries JSON
- scripts/     installer + Gemini pregen + editable prompt template
- caddy/       snippet mounting /collage on existing Caddy
- forwarding/  optional HA / MQTT / Cloudflare configs

Default install: http://birdnet.local/collage/ with no auth.

Upstream BirdNET-Pi README preserved at README.upstream.md.
This commit is contained in:
Twarner491
2026-05-28 10:14:25 -07:00
parent 88985a3a6f
commit d0ad1454c4
705 changed files with 2276 additions and 154 deletions
+134
View File
@@ -0,0 +1,134 @@
<?php
// /home/monalisa/BirdSongs/Extracted/cutout.php — dynamic background-removed
// bird-photo facade. Called by the bird.onethreenine.net Cloudflare Worker
// (which fronts /api/img?sci=<name>).
//
// Lives in the Caddy file-server root so the BirdNET-Pi auto-update doesn't
// touch it. Each new species hits this once: download Wikipedia/Macaulay
// image → rembg via /usr/local/bin/rembg-cli → cache transparent PNG to
// disk under cutouts/. Subsequent requests are instant readfile() from cache.
//
// Auth: the Caddy site block 403s anything missing X-BirdNET-Proxy-Token at
// the top, so this script inherits the Worker-only access guarantee.
//
// Pre-reqs (run once via /install/rembg-setup.sh):
// - /home/monalisa/rembg-env/bin/rembg (Python venv)
// - /usr/local/bin/rembg-cli (wrapper)
// - /home/monalisa/BirdSongs/Extracted/cutouts/ (cache dir, monalisa-owned)
declare(strict_types=1);
$sci = trim((string)($_GET['sci'] ?? ''));
if ($sci === '') {
http_response_code(400);
echo 'sci required';
exit;
}
// Slugify scientific name for the cache filename.
$slug = preg_replace('/[^a-z0-9]+/', '-', strtolower($sci));
$slug = trim((string)$slug, '-');
$cacheDir = '/home/monalisa/BirdSongs/Extracted/cutouts';
$cachePath = "$cacheDir/$slug.png";
function serve_png(string $path): void {
header('Content-Type: image/png');
header('Cache-Control: public, max-age=2592000');
header('Content-Length: ' . filesize($path));
readfile($path);
exit;
}
// Cache hit — short-circuit.
if (is_file($cachePath) && filesize($cachePath) > 1024) {
serve_png($cachePath);
}
// Make sure the cache dir exists (first run).
if (!is_dir($cacheDir)) @mkdir($cacheDir, 0755, true);
// Resolve a source image URL. Wikipedia summary first (free, fast,
// no auth, has a clean originalimage for most birds).
$ctx = stream_context_create([
'http' => [
'header' => "User-Agent: apartment-birds/1.0 (twarner491@gmail.com)\r\n",
'timeout' => 12,
],
]);
$wpUrl = 'https://en.wikipedia.org/api/rest_v1/page/summary/' . rawurlencode($sci);
$wpJson = @file_get_contents($wpUrl, false, $ctx);
$srcUrl = null;
if ($wpJson !== false) {
$j = json_decode($wpJson, true);
$srcUrl = $j['originalimage']['source'] ?? $j['thumbnail']['source'] ?? null;
}
if (!$srcUrl) {
http_response_code(404);
echo 'no upstream image for ' . htmlspecialchars($sci);
exit;
}
// Download the source image.
$imgBytes = @file_get_contents($srcUrl, false, $ctx);
if (!$imgBytes || strlen($imgBytes) < 1024) {
http_response_code(503);
echo 'failed to fetch source image';
exit;
}
// Run rembg via the wrapper. We use temp files because rembg's CLI prefers
// real paths. u2netp is the lightweight model (~50MB peak RAM) — important
// on the Pi 3B+ (1GB total RAM). Call with --post-process-mask to clean
// up edges.
$tmpIn = tempnam('/tmp', 'rembg-in-') . '.jpg';
$tmpOut = tempnam('/tmp', 'rembg-out-') . '.png';
file_put_contents($tmpIn, $imgBytes);
$cmd = sprintf(
'/usr/local/bin/rembg-cli i -m u2netp -ppm %s %s 2>&1',
escapeshellarg($tmpIn),
escapeshellarg($tmpOut)
);
$out = shell_exec($cmd);
@unlink($tmpIn);
if (!is_file($tmpOut) || filesize($tmpOut) < 1024) {
@unlink($tmpOut);
http_response_code(500);
header('Content-Type: text/plain');
echo "rembg failed:\n" . ($out ?? '(no output)');
exit;
}
// 1. Alpha-crop to bounding box so each PNG == the bird's actual shape
// (no transparent padding around it). Lets the layout pack tiles tight.
// 2. Resize down to a max edge of 800px so the cache stays small.
$im = @imagecreatefrompng($tmpOut);
if ($im !== false) {
$cropped = @imagecropauto($im, IMG_CROP_TRANSPARENT);
if ($cropped !== false) {
imagedestroy($im);
$im = $cropped;
}
$w = imagesx($im); $h = imagesy($im);
$max = 800;
if ($w > $max || $h > $max) {
$scale = $max / max($w, $h);
$nw = (int)($w * $scale); $nh = (int)($h * $scale);
$resized = imagecreatetruecolor($nw, $nh);
imagealphablending($resized, false);
imagesavealpha($resized, true);
imagecopyresampled($resized, $im, 0, 0, 0, 0, $nw, $nh, $w, $h);
imagedestroy($im);
$im = $resized;
}
imagealphablending($im, false);
imagesavealpha($im, true);
imagepng($im, $tmpOut, 6);
imagedestroy($im);
}
// Cache + serve.
copy($tmpOut, $cachePath);
@unlink($tmpOut);
serve_png($cachePath);