[PROD] refined php int

This commit is contained in:
Twarner491
2026-05-28 15:39:58 -07:00
parent ec167eac7b
commit 038b17c6c1
8 changed files with 48 additions and 42 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ curl -s https://raw.githubusercontent.com/Twarner491/AvianVisitors/avian-visitor
Clones this fork, installs BirdNET-Pi, symlinks the AvianVisitors overlay into the Caddy web root. Takes 20-40 minutes. Reboots when done.
Collage: `http://birdnet.local/`. Stock BirdNET-Pi UI: `http://birdnet.local/index.php` (also linked from the menu drawer).
Collage: `http://birdnet.local/`. Stock BirdNET-Pi UI: `http://birdnet.local/index.php`. The menu button in the top right opens an admin overlay with settings, system, log, and tool panels.
---
+2 -2
View File
@@ -93,8 +93,8 @@ switch ($action) {
case 'recent': {
// Cap raised to 1,000,000 hours (~114 years) so the frontend's
// "ALL" button can effectively turn off the time filter without
// needing a separate code path.
// "ALL" button can turn off the time filter without needing a
// separate code path.
$hours = max(1, min(1000000, (int)($_GET['hours'] ?? 24)));
// species-collapsed view: one row per species seen in the window,
// with the file of its highest-confidence detection inside the window.
+3 -3
View File
@@ -5,7 +5,7 @@
// 1. ../assets/illustrations/<slug>.png (450+ bundled kachō-e renders)
// 2. ../assets/cutouts/<slug>.png (background-removed photo)
// 3. cached rembg of a Wikipedia photo at $HOME/BirdSongs/Extracted/cutouts/
// 4. fresh Wikipedia rembg cache (skipped gracefully if rembg unset)
// 4. fresh Wikipedia -> rembg -> cache (skipped gracefully if rembg unset)
//
// The frontend's <img src> points here for every species - bundled
// hits return instantly; cold misses fall through to the dynamic path.
@@ -76,8 +76,8 @@ 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
// than burning a Wikipedia request we can't use.
// PATH - the resolver 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)) {
http_response_code(404);
+3 -3
View File
@@ -7,7 +7,7 @@
// $HOME/BirdSongs/Extracted/By_Date/YYYY-MM-DD/<Common_Name>/<base>.mp3
// (with a matching .png next to it). Common_Name is the SPACE-stripped
// English common name (e.g. "Anna's_Hummingbird"), NOT the scientific
// name. We resolve sci common via birds.json under BirdNET-Pi/scripts/
// name. We resolve sci -> common via birds.json under BirdNET-Pi/scripts/
// and walk the directory tree newest-first.
//
@@ -96,7 +96,7 @@ if ($file !== '') {
readfile($path);
exit;
}
// ---- Resolve scientific name common name (with underscores) ----
// ---- Resolve scientific name -> common name (with underscores) ----
function resolve_common(string $sci): ?string {
// Try birds.json first (preferred - has clean sci/com pairs).
foreach ([dirname(__DIR__, 3) . '/BirdNET-Pi/scripts/birds.json'] as $f) {
@@ -131,7 +131,7 @@ function resolve_common(string $sci): ?string {
$common = resolve_common($sci);
if ($common === null) {
// Last-ditch: try the scientific name itself, with spaces underscores.
// Last-ditch: try the scientific name itself, with spaces -> underscores.
// (Some BirdNET dirs are keyed by sci name.)
$common = str_replace(' ', '_', $sci);
}
+2 -2
View File
@@ -5,8 +5,8 @@
// instead of .mp3.
//
// Endpoints:
// ?sci=<sci_name> newest spectrogram for that species
// ?file=<original-base>.mp3 spectrogram next to that specific
// ?sci=<sci_name> -> newest spectrogram for that species
// ?file=<original-base>.mp3 -> spectrogram next to that specific
// recording (atlas modal uses this so the
// strip below each play button is the
// spectrogram for that recording, not
+32 -26
View File
@@ -45,7 +45,7 @@
function setTitleForView(i) {
var next = VIEW_TITLES[i];
if (!staticTitle || staticTitle.textContent === next) return;
// Fade out swap text fade in. The opacity transition is 240ms;
// Fade out -> swap text -> fade in. The opacity transition is 240ms;
// we swap at ~half that so the eye doesn't catch the text change.
staticHead.classList.add('swap-out');
setTimeout(function () {
@@ -155,7 +155,7 @@
n <= 12 ? 0.40 :
n <= 24 ? 0.34 :
0.28,
// Count area exponent. ~0.65 keeps the visual hierarchy
// Count -> area exponent. ~0.65 keeps the visual hierarchy
// legible (n=400 reads ~5× bigger than n=30) without the
// loudest bird drowning everything else.
countExp: 0.65,
@@ -610,7 +610,7 @@
byHour: new Array(24).fill(0), // [hour-of-day] detections
};
// Map sci all-time detection count, populated from lifelist for atlas.
// Map sci -> all-time detection count, populated from lifelist for atlas.
var speciesTotals = {};
function fetchJson(url) {
@@ -654,20 +654,20 @@
// toward the "recent" end of the gradient render in deeper ink; older
// bars fade to a warm light grey. Same hue family throughout.
function barColor(t) {
// t = 0 (outer / newest) 1 (inner / oldest).
// t = 0 (outer / newest) -> 1 (inner / oldest).
// Monochromatic ink palette: same warm hue as the title text
// (--ink: #1a1612 ≈ HSL 25, 14%, 9%). Newest hours render in deep
// ink so the outer perimeter reads bold; older hours fade to a
// warm light grey, the chart looks like a hand-pulled engraving.
var hue = 25; // warm-grey hue, matches --ink family
var sat = 12 - t * 8; // 12% 4%
var light = 14 + t * 50; // 14% (near-black) 64% (light grey)
var sat = 12 - t * 8; // 12% -> 4%
var light = 14 + t * 50; // 14% (near-black) -> 64% (light grey)
return 'hsl(' + hue + ', ' + sat.toFixed(0) + '%, ' + light.toFixed(0) + '%)';
}
// Editorial detection timeline. One column per species; the black
// square's height up the column encodes detection count (y axis),
// columns run leftright oldestnewest detection (x axis). A
// columns run left->right oldest->newest detection (x axis). A
// rotated species label sits just above each square. Y-axis count
// ticks on the left, X-axis time labels on the bottom. Always fits
// the viewport - column widths flex, square size steps down as the
@@ -918,7 +918,7 @@
var lifelist = (DATA.lifelist && DATA.lifelist.species) || [];
var recent = (DATA.recent && DATA.recent.species) || [];
// Window count lookup: sci count in current window.
// Window count lookup: sci -> count in current window.
var winBySci = {};
recent.forEach(function (s) { winBySci[s.sci] = +s.n; });
@@ -1271,7 +1271,7 @@
// native:true they navigate in-page; otherwise they keep the old
// open-in-new-tab behavior for the legacy BirdNET-Pi screens.
var linksHtml = menu.map(function (it) {
var label = (it.label || '').replace('today's detections', 'today');
var label = (it.label || '');
var attrs = it.native ? '' : ' target="_blank" rel="noopener"';
var cls = it.native ? '' : ' class="ext"';
return '<a' + cls + ' href="' + it.href + '"' + attrs + '><span>' + label + '</span></a>';
@@ -1394,7 +1394,7 @@
var idx = Math.round(lo + (hi - lo) * Math.pow(t, 1.6));
var v = (bins[idx] || 0) / 255;
var e = v * v * (3 - 2 * v);
// Paper (245,240,230) ink (26,22,18) ramp.
// Paper (245,240,230) -> ink (26,22,18) ramp.
var r = 245 + Math.round((26 - 245) * e);
var g = 240 + Math.round((22 - 240) * e);
var b = 230 + Math.round((18 - 230) * e);
@@ -1439,7 +1439,7 @@
});
}
// Pending changes (key value), saved on click of the Save button.
// Pending changes (key -> value), saved on click of the Save button.
var pending = {};
function setSaveState(msg, cls) {
@@ -1456,7 +1456,7 @@
var v = cfg.values || {};
var preserve = cfg.preserve;
var html = ''
+ settingsToggle('preserve', 'Preserve all recordings', 'don't auto-delete', preserve)
+ settingsToggle('preserve', 'Preserve all recordings', "don't auto-delete", preserve)
+ settingsSlider('CONFIDENCE', 'Confidence threshold', 'min score to log a detection', v.CONFIDENCE, 0.1, 0.95, 0.05, 2)
+ settingsSlider('SENSITIVITY', 'Sensitivity', 'analyzer sensitivity', v.SENSITIVITY, 0.5, 1.5, 0.05, 2)
+ settingsSlider('OVERLAP', 'Chunk overlap', 'seconds analyzed per pass', v.OVERLAP, 0, 2.5, 0.1, 1)
@@ -2053,7 +2053,7 @@
var preserve = cfg.preserve;
adminBody.innerHTML =
'<div class="admin-settings">'
+ settingsToggle('preserve', 'Preserve all recordings', 'don't auto-delete', preserve)
+ settingsToggle('preserve', 'Preserve all recordings', "don't auto-delete", preserve)
+ settingsSlider('CONFIDENCE', 'Confidence threshold', 'min score to log a detection', v.CONFIDENCE, 0.1, 0.95, 0.05, 2)
+ settingsSlider('SENSITIVITY', 'Sensitivity', 'analyzer sensitivity', v.SENSITIVITY, 0.5, 1.5, 0.05, 2)
+ settingsSlider('OVERLAP', 'Chunk overlap', 'seconds analyzed per pass', v.OVERLAP, 0, 2.5, 0.1, 1)
@@ -2130,7 +2130,13 @@
sys.disk_birds && sys.disk_birds.used_pct > 92 ? 'warn' : '');
var audio = sys.audio || {}, cards = audio.arecord_l || [];
var mic = cards.find ? cards.find(function (c) { return /usb-audio|microphone|mic/i.test(c); }) : null;
html += adminCard('audio device', mic || cards[0] || 'no cards', '');
// Without a USB mic, /proc/asound/cards only lists the Pi's HDMI
// audio outputs - which aren't an input source. Flag that clearly
// rather than showing "audio device: vc4hdmi0" as if it were a mic.
html += adminCard('audio device',
mic || (cards.length ? 'no microphone attached' : 'no audio devices'),
mic ? '' : (cards[0] || ''),
mic ? '' : 'warn');
html += '</div>';
html += '<h2 class="admin-section-head">services</h2>';
@@ -2312,7 +2318,7 @@
var m = location.hash.match(/^#admin=([a-z]+)/);
return m ? m[1] : null;
}
// #about - brief explainer popup; reached via /about (302 /#about)
// #about - brief explainer popup; reached via /about (302 -> /#about)
// or the masthead eyebrow. aria-hidden drives the CSS fade/slide.
function openAbout() { document.getElementById('about-modal').setAttribute('aria-hidden', 'false'); }
function closeAbout() { document.getElementById('about-modal').setAttribute('aria-hidden', 'true'); }
@@ -2330,7 +2336,7 @@
if (location.hash === '#about') openAbout();
window.addEventListener('hashchange', syncRouter);
// Modal interactions: backdrop / close button clear the hash.
// Modal interactions: backdrop / close button -> clear the hash.
document.getElementById('detail-modal').addEventListener('click', function (ev) {
if (ev.target.dataset && ev.target.dataset.close === '1') {
if (location.hash) { location.hash = ''; } else { closeDetailModal(); }
@@ -2344,7 +2350,7 @@
});
// About popup: backdrop / close / explore button all carry data-close,
// which clears the hash and routes through syncRouter closeAbout.
// which clears the hash and routes through syncRouter -> closeAbout.
// The masthead eyebrow opens it; Escape dismisses it.
document.getElementById('about-modal').addEventListener('click', function (ev) {
if (ev.target.dataset && ev.target.dataset.close === '1') {
@@ -2452,7 +2458,7 @@
var bins = FFT_SIZE >> 1;
var nyquist = sr / 2;
// Frequency-band mapping (Hz bin) for the bird-relevant band.
// Frequency-band mapping (Hz -> bin) for the bird-relevant band.
// Most North American songbirds + corvids range 250 Hz - 8 kHz, but
// hummingbirds, kinglets, and warblers reach 12 kHz. Push the cap
// up so we don't miss the high-frequency tail.
@@ -2483,7 +2489,7 @@
data[p] = BG_R; data[p + 1] = BG_G; data[p + 2] = BG_B; data[p + 3] = 255;
}
// Precompute row bin map (log-ish so low freqs get more space).
// Precompute row -> bin map (log-ish so low freqs get more space).
var rowToBin = new Int32Array(H);
for (var row = 0; row < H; row++) {
var t = 1 - row / (H - 1); // 1 at top, 0 at bottom
@@ -2503,11 +2509,11 @@
var bin2 = rowToBin[row2];
var re = real[bin2], im = imag[bin2];
var mag = Math.sqrt(re * re + im * im);
// log compress; -75 .. -10 dB 0 .. 1
// log compress; -75 .. -10 dB -> 0 .. 1
var db = 20 * Math.log10(mag + 1e-9);
var v = (db + 75) / 65;
if (v < 0) v = 0; else if (v > 1) v = 1;
// Ink-on-paper palette: low energy paper, high energy ink.
// Ink-on-paper palette: low energy -> paper, high energy -> ink.
// Smoothstep for a softer falloff between the two extremes.
var e = v * v * (3 - 2 * v);
var r = BG_R + Math.round((FG_R - BG_R) * e);
@@ -2593,11 +2599,11 @@
var playBtn = ev.target.closest('.play');
if (playBtn) {
// Play / pause toggle. Three cases:
// (a) clicking the playing row's button pause (KEEP audio
// (a) clicking the playing row's button -> pause (KEEP audio
// alive so the user can scrub then resume).
// (b) clicking a paused row's button (it's still modalRecBtn,
// audio still alive, just paused) resume from cursor.
// (c) clicking a different row's button stop the old, start
// audio still alive, just paused) -> resume from cursor.
// (c) clicking a different row's button -> stop the old, start
// the new.
var prow = playBtn.closest('.rec-row');
var pfile = prow && prow.dataset.file;
@@ -2653,7 +2659,7 @@
return;
}
// Row click anywhere else toggle strip open/closed.
// Row click anywhere else -> toggle strip open/closed.
var row = ev.target.closest('.rec-row');
if (!row) return;
var willExpand = !row.classList.contains('expanded');
@@ -2727,7 +2733,7 @@
if (location.hash !== '#sci=' + encodeURIComponent(sci)) {
location.hash = '#sci=' + encodeURIComponent(sci);
} else {
// Same hash still re-highlight (the user clicked it again).
// Same hash -> still re-highlight (the user clicked it again).
go(2); highlightAtlas(sci);
}
}
+1 -1
View File
@@ -1058,7 +1058,7 @@
}
/* ============ Detail modal (atlas card expansion) ============
Click a bird card this fades in over a soft ink backdrop with
Click a bird card -> this fades in over a soft ink backdrop with
the full sketch, taxonomic info, Wikipedia summary, and a
scrollable list of every past audio capture for the species. */
/* /about - a brief explainer popup floated over the index page.
+4 -4
View File
@@ -73,9 +73,9 @@ create_necessary_dirs() {
# reachable at /avian/. The five frontend files at the EXTRACTED root
# make the collage the default index for http://birdnet.local/ -
# the matching try_files override in update_caddyfile.sh teaches
# php_fastcgi to prefer index.html over index.php at the root, so
# BirdNET-Pi's stock UI moves to http://birdnet.local/index.php
# (still linked from the AvianVisitors menu drawer).
# php_fastcgi to prefer index.html over index.php at the root. The
# stock BirdNET-Pi UI stays reachable at http://birdnet.local/index.php
# for anyone who wants to drop into the legacy admin pages.
if [ -d $my_dir/avian ]; then
sudo -u ${USER} ln -fs $my_dir/avian ${EXTRACTED}/avian
sudo -u ${USER} ln -fs $my_dir/avian/frontend/index.html ${EXTRACTED}/index.html
@@ -94,7 +94,7 @@ create_necessary_dirs() {
sudo -u ${USER} ln -fs $my_dir/scripts/todays_detections.php ${EXTRACTED}
sudo -u ${USER} ln -fs $my_dir/scripts/history.php ${EXTRACTED}
sudo -u ${USER} ln -fs $my_dir/scripts/weekly_report.php ${EXTRACTED}
# favicon.ico AvianVisitors PNG when the overlay is present (modern
# favicon.ico -> AvianVisitors PNG when the overlay is present (modern
# browsers accept image/png for the .ico path); fall back to the stock
# BirdNET-Pi favicon.ico otherwise so plain installs still get an icon.
if [ -d $my_dir/avian ]; then