diff --git a/README.md b/README.md index 85afc44..cb0317a 100644 --- a/README.md +++ b/README.md @@ -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. --- diff --git a/avian/api/birdnet-api.php b/avian/api/birdnet-api.php index 4fc8aa3..f2a20b4 100644 --- a/avian/api/birdnet-api.php +++ b/avian/api/birdnet-api.php @@ -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. diff --git a/avian/api/cutout.php b/avian/api/cutout.php index f17dad2..a7f20bd 100644 --- a/avian/api/cutout.php +++ b/avian/api/cutout.php @@ -5,7 +5,7 @@ // 1. ../assets/illustrations/.png (450+ bundled kachō-e renders) // 2. ../assets/cutouts/.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 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); diff --git a/avian/api/recording.php b/avian/api/recording.php index 1b7a59d..33d3364 100644 --- a/avian/api/recording.php +++ b/avian/api/recording.php @@ -7,7 +7,7 @@ // $HOME/BirdSongs/Extracted/By_Date/YYYY-MM-DD//.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); } diff --git a/avian/api/spectrogram.php b/avian/api/spectrogram.php index 5583e39..169fdcb 100644 --- a/avian/api/spectrogram.php +++ b/avian/api/spectrogram.php @@ -5,8 +5,8 @@ // instead of .mp3. // // Endpoints: -// ?sci= → newest spectrogram for that species -// ?file=.mp3 → spectrogram next to that specific +// ?sci= -> newest spectrogram for that species +// ?file=.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 diff --git a/avian/frontend/apt.js b/avian/frontend/apt.js index c6cb981..4017aef 100644 --- a/avian/frontend/apt.js +++ b/avian/frontend/apt.js @@ -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 left→right oldest→newest 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 '' + label + ''; @@ -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 = '
' - + 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 += '
'; html += '

services

'; @@ -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); } } diff --git a/avian/frontend/styles.css b/avian/frontend/styles.css index d2868c0..dd46c3c 100644 --- a/avian/frontend/styles.css +++ b/avian/frontend/styles.css @@ -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. diff --git a/scripts/install_services.sh b/scripts/install_services.sh index a2dd5e1..882cd91 100755 --- a/scripts/install_services.sh +++ b/scripts/install_services.sh @@ -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