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:
@@ -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);
|
||||
Reference in New Issue
Block a user