diff --git a/birdnet-pi/CHANGELOG.md b/birdnet-pi/CHANGELOG.md index 3b15fd555..cac0bdc64 100644 --- a/birdnet-pi/CHANGELOG.md +++ b/birdnet-pi/CHANGELOG.md @@ -1,3 +1,5 @@ +- Significantly improve the SPECIES_CONVERTER option, and add a webui when the option is enabled + ## 0.13-28 (17-05-2024) - Improve code clarity by separating modifications of code to make it work, and new features specific to the addon - New option SPECIES_CONVERTER: if enabled, you need to put in the file /config/convert_species_list.txt the list of species you want to convert (example : Falco subbuteo_Faucon hobereau;Falco tinnunculus_Faucon Crécerelle). It will convert on the fly the specie when detected. This is not enabled by default as can be a cause for issues diff --git a/birdnet-pi/config.json b/birdnet-pi/config.json index 73ab239ab..1147e74d6 100644 --- a/birdnet-pi/config.json +++ b/birdnet-pi/config.json @@ -102,6 +102,6 @@ "udev": true, "url": "https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-pi", "usb": true, - "version": "0.13-28", + "version": "0.13-29", "video": true } diff --git a/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh b/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh index 9034ec4e6..59dfd5a69 100755 --- a/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh +++ b/birdnet-pi/rootfs/etc/cont-init.d/72-newfeatures.sh @@ -55,32 +55,17 @@ fi # Add species conversion system if bashio::config.true "SPECIES_CONVERTER"; then - bashio::log.yellow "... adding feature of SPECIES_CONVERTER, please see README to use" + bashio::log.yellow "... adding feature of SPECIES_CONVERTER, a new tab is added to your Tools" touch /config/convert_species_list.txt chown pi:pi /config/convert_species_list.txt sudo -u pi ln -fs /config/convert_species_list.txt "$HOME"/BirdNET-Pi/ # Not useful sed -i "/exclude_species_list.txt/a sudo -u pi ln -fs /config/convert_species_list.txt $HOME/BirdNET-Pi/scripts/" "$HOME"/BirdNET-Pi/scripts/clear_all_data.sh sed -i "/exclude_species_list.txt/a sudo -u pi ln -fs /config/convert_species_list.txt $HOME/BirdNET-Pi/scripts/" "$HOME"/BirdNET-Pi/scripts/install_services.sh - # Change server - sed -i "/INTERPRETER, M_INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST/c INTERPRETER, M_INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST = (None, None, None, None, None)" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/global INCLUDE_LIST, EXCLUDE_LIST/c\ global INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST, CONVERT_DICT" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/exclude_species_list.txt/a\ CONVERT_DICT = {row.split(';')[0]: row.split(';')[1] for row in CONVERT_LIST}" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/exclude_species_list.txt/a\ CONVERT_LIST = loadCustomSpeciesList(os.path.expanduser(\"~/BirdNET-Pi/convert_species_list.txt\"))" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "s|entry\[0\]|converted_entry|g" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "s|if converted_entry in|if entry\[0\] in|g" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/for entry in entries/a\ converted_entry = entry[0]" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/for entry in entries/a\ else :" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/for entry in entries/a\ log.info('WARNING : ' + entry[0] + ' converted to ' + converted_entry)" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/for entry in entries/a\ converted_entry = CONVERT_DICT.get(entry[0], entry[0])" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/for entry in entries/a\ if entry[0] in CONVERT_DICT:" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/for entry in entries/a\ if entry[1] >= conf.getfloat('CONFIDENCE'):" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "/converted_entry in INCLUDE_LIST or len(INCLUDE_LIST)/c\ if ((converted_entry in INCLUDE_LIST or len(INCLUDE_LIST) == 0)" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "s| d = Detection| d = Detection|g" "$HOME"/BirdNET-Pi/scripts/server.py - sed -i "s| confident_detections| confident_detections|g" "$HOME"/BirdNET-Pi/scripts/server.py - curl -o /home/pi/BirdNET-Pi/scripts/convert_list.php https://raw.githubusercontent.com/alexbelgium/BirdNET-Pi/patch-2_species_conversion/scripts/convert_list.php - chmod 777 /home/pi/BirdNET-Pi/scripts/convert_list.php - #sed -i '/Excluded Species List/\ ' "$HOME"/BirdNET-Pi/homepage/views.php + # Add files + mv -f /helpers/convert_list/convert_list.php "$HOME"/BirdNET-Pi/scripts/convert_list.php && chown pi:pi "$HOME"/BirdNET-Pi/scripts/convert_list.php + mv -f /helpers/convert_list/server.py "$HOME"/BirdNET-Pi/scripts/server.py && chown pi:pi "$HOME"/BirdNET-Pi/scripts/server.py + mv -f /helpers/convert_list/views.php "$HOME"/BirdNET-Pi/homepage/views.php && chown pi:pi "$HOME"/BirdNET-Pi/homepage/views.php fi echo " " diff --git a/birdnet-pi/rootfs/helpers/change_id/bird.svg b/birdnet-pi/rootfs/helpers/change_id/bird.svg new file mode 100644 index 000000000..2fa6ab0dc --- /dev/null +++ b/birdnet-pi/rootfs/helpers/change_id/bird.svg @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/birdnet-pi/rootfs/helpers/changeidentification.sh b/birdnet-pi/rootfs/helpers/change_id/birdnet_changeidentification.sh old mode 100755 new mode 100644 similarity index 100% rename from birdnet-pi/rootfs/helpers/changeidentification.sh rename to birdnet-pi/rootfs/helpers/change_id/birdnet_changeidentification.sh diff --git a/birdnet-pi/rootfs/helpers/change_id/play.php.txt b/birdnet-pi/rootfs/helpers/change_id/play.php.txt new file mode 100644 index 000000000..0c1827fe8 --- /dev/null +++ b/birdnet-pi/rootfs/helpers/change_id/play.php.txt @@ -0,0 +1,705 @@ +busyTimeout(1000); + +if(isset($_GET['deletefile'])) { + ensure_authenticated('You must be authenticated to delete files.'); + if (preg_match('~^.*(\.\.\/).+$~', $_GET['deletefile'])) { + echo "Error"; + die(); + } + $db_writable = new SQLite3('./scripts/birds.db', SQLITE3_OPEN_READWRITE); + $db->busyTimeout(1000); + $statement1 = $db_writable->prepare('DELETE FROM detections WHERE File_Name = :file_name LIMIT 1'); + ensure_db_ok($statement1); + $statement1->bindValue(':file_name', explode("/", $_GET['deletefile'])[2]); + $file_pointer = $home."/BirdSongs/Extracted/By_Date/".$_GET['deletefile']; + if (!exec("sudo rm $file_pointer 2>&1 && sudo rm $file_pointer.png 2>&1", $output)) { + echo "OK"; + } else { + echo "Error - file deletion failed : " . implode(", ", $output) . "
"; + } + $result1 = $statement1->execute(); + if ($result1 === false || $db_writable->changes() === 0) { + echo "Error - database line deletion failed : " . $db_writable->lastErrorMsg(); + } + $db_writable->close(); + die(); +} + +if(isset($_GET['excludefile'])) { + ensure_authenticated('You must be authenticated to change the protection of files.'); + if(!file_exists($home."/BirdNET-Pi/scripts/disk_check_exclude.txt")) { + file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "##start\n##end\n"); + } + if(isset($_GET['exclude_add'])) { + $myfile = fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", "a") or die("Unable to open file!"); + $txt = $_GET['excludefile']; + fwrite($myfile, $txt."\n"); + fwrite($myfile, $txt.".png\n"); + fclose($myfile); + echo "OK"; + die(); + } else { + $lines = file($home."/BirdNET-Pi/scripts/disk_check_exclude.txt"); + $search = $_GET['excludefile']; + + $result = ''; + foreach($lines as $line) { + if(stripos($line, $search) === false && stripos($line, $search.".png") === false) { + $result .= $line; + } + } + file_put_contents($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", $result); + echo "OK"; + die(); + } +} + +if(isset($_GET['getlabels'])) { + $labels = file('/home/pi/BirdNET-Pi/model/labels.txt', FILE_IGNORE_NEW_LINES); + echo json_encode($labels); + die(); +} + +if(isset($_GET['changefile']) && isset($_GET['newname'])) { + ensure_authenticated('You must be authenticated to delete files.'); + if (preg_match('~^.*(\.\.\/).+$~', $_GET['changefile'])) { + echo "Error"; + die(); + } + $oldname = basename(urldecode($_GET['changefile'])); + $newname = urldecode($_GET['newname']); + if (!exec("$home/BirdNET-Pi/scripts/birdnet_changeidentification.sh \"$oldname\" \"$newname\" log_errors 2>&1", $output)) { + echo "OK"; + } else { + echo "Error : " . implode(", ", $output) . "
"; + } + die(); +} + +$shifted_path = $home."/BirdSongs/Extracted/By_Date/shifted/"; + +if(isset($_GET['shiftfile'])) { + ensure_authenticated('You cannot shift files for this installation'); + + $filename = $_GET['shiftfile']; + $pp = pathinfo($filename); + $dir = $pp['dirname']; + $fn = $pp['filename']; + $ext = $pp['extension']; + $pi = $home."/BirdSongs/Extracted/By_Date/"; + + if(isset($_GET['doshift'])) { + $freqshift_tool = $config['FREQSHIFT_TOOL']; + + if ($freqshift_tool == "ffmpeg") { + $cmd = "sudo /usr/bin/nohup /usr/bin/ffmpeg -y -i ".escapeshellarg($pi.$filename)." -af \"rubberband=pitch=".$config['FREQSHIFT_LO']."/".$config['FREQSHIFT_HI']."\" ".escapeshellarg($shifted_path.$filename).""; + shell_exec("sudo mkdir -p ".$shifted_path.$dir." && ".$cmd); + + } else if ($freqshift_tool == "sox") { + //linux.die.net/man/1/sox + $soxopt = "-q"; + $soxpitch = $config['FREQSHIFT_PITCH']; + $cmd = "sudo /usr/bin/nohup /usr/bin/sox ".escapeshellarg($pi.$filename)." ".escapeshellarg($shifted_path.$filename)." pitch ".$soxopt." ".$soxpitch; + shell_exec("sudo mkdir -p ".$shifted_path.$dir." && ".$cmd); + } + } else { + $cmd = "sudo rm -f " . escapeshellarg($shifted_path.$filename); + shell_exec($cmd); + } + + echo "OK"; + die(); +} + +if(isset($_GET['bydate'])){ + $statement = $db->prepare('SELECT DISTINCT(Date) FROM detections GROUP BY Date ORDER BY Date DESC'); + ensure_db_ok($statement); + $result = $statement->execute(); + $view = "bydate"; + + #Specific Date +} elseif(isset($_GET['date'])) { + $date = $_GET['date']; + session_start(); + $_SESSION['date'] = $date; + if(isset($_GET['sort']) && $_GET['sort'] == "occurrences") { + $statement = $db->prepare("SELECT DISTINCT(Com_Name) FROM detections WHERE Date == \"$date\" GROUP BY Com_Name ORDER BY COUNT(*) DESC"); + } else { + $statement = $db->prepare("SELECT DISTINCT(Com_Name) FROM detections WHERE Date == \"$date\" ORDER BY Com_Name"); + } + ensure_db_ok($statement); + $result = $statement->execute(); + $view = "date"; + + #By Species +} elseif(isset($_GET['byspecies'])) { + if(isset($_GET['sort']) && $_GET['sort'] == "occurrences") { + $statement = $db->prepare('SELECT DISTINCT(Com_Name) FROM detections GROUP BY Com_Name ORDER BY COUNT(*) DESC'); + } else { + $statement = $db->prepare('SELECT DISTINCT(Com_Name) FROM detections ORDER BY Com_Name ASC'); + } + session_start(); + ensure_db_ok($statement); + $result = $statement->execute(); + $view = "byspecies"; + + #Specific Species +} elseif(isset($_GET['species'])) { + $species = htmlspecialchars_decode($_GET['species'], ENT_QUOTES); + session_start(); + $_SESSION['species'] = $species; + $statement = $db->prepare("SELECT * FROM detections WHERE Com_Name == \"$species\" ORDER BY Com_Name"); + ensure_db_ok($statement); + $statement3 = $db->prepare("SELECT Date, Time, Sci_Name, MAX(Confidence), File_Name FROM detections WHERE Com_Name == \"$species\" ORDER BY Com_Name"); + ensure_db_ok($statement3); + $result = $statement->execute(); + $result3 = $statement3->execute(); + $view = "species"; +} else { + unset($_SESSION['species']); + unset($_SESSION['date']); + $view = "choose"; +} + +if (get_included_files()[0] === __FILE__) { + echo ' + + + + + '; +} + +?> + + + +
+ +
+
+ + + + +
+
+
+ +
+ + +fetchArray(SQLITE3_ASSOC)){ + $date = $results['Date']; + if(realpath($home."/BirdSongs/Extracted/By_Date/".$date) !== false){ + echo "";}} + + #By Species + } elseif($view == "byspecies") { + $birds = array(); + while($results=$result->fetchArray(SQLITE3_ASSOC)) + { + $name = $results['Com_Name']; + $birds[] = $name; + } + + if(count($birds) > 45) { + $num_cols = 3; + } else { + $num_cols = 1; + } + $num_rows = ceil(count($birds) / $num_cols); + + for ($row = 0; $row < $num_rows; $row++) { + echo ""; + + for ($col = 0; $col < $num_cols; $col++) { + $index = $row + $col * $num_rows; + + if ($index < count($birds)) { + ?> + + "; + } + } + + echo ""; + } + } elseif($view == "date") { + $birds = array(); +while($results=$result->fetchArray(SQLITE3_ASSOC)) +{ + $name = $results['Com_Name']; + $dir_name = str_replace("'", '', $name); + if(realpath($home."/BirdSongs/Extracted/By_Date/".$date."/".str_replace(" ", "_", $dir_name)) !== false){ + $birds[] = $name; + } +} + +if(count($birds) > 45) { + $num_cols = 3; +} else { + $num_cols = 1; +} +$num_rows = ceil(count($birds) / $num_cols); + +for ($row = 0; $row < $num_rows; $row++) { + echo ""; + + for ($col = 0; $col < $num_cols; $col++) { + $index = $row + $col * $num_rows; + + if ($index < count($birds)) { + ?> + + "; + } + } + + echo ""; +} + + #Choose + } else { + echo " + "; + } + + echo "
+
+ +
+ +
+
"; +} + +#Specific Species +if(isset($_GET['species'])){ ?> +
+
+ + + + +
+ type="checkbox" name="only_excluded" onChange="submit()"> + +
+
+prepare("SELECT * FROM detections where Com_Name == \"$name\" AND Date == \"$date\" ORDER BY Confidence DESC"); + } else { + $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" AND Date == \"$date\" ORDER BY Time DESC"); + } +} else { + if(isset($_GET['sort']) && $_GET['sort'] == "confidence") { + $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" ORDER BY Confidence DESC"); + } else { + $statement2 = $db->prepare("SELECT * FROM detections where Com_Name == \"$name\" ORDER BY Date DESC, Time DESC"); + } +} +ensure_db_ok($statement2); +$result2 = $statement2->execute(); +$num_rows = 0; +while ($result2->fetchArray(SQLITE3_ASSOC)) { + $num_rows++; +} +$result2->reset(); // reset the pointer to the beginning of the result set +echo " + + + "; + $iter=0; + while($results=$result2->fetchArray(SQLITE3_ASSOC)) + { + $comname = preg_replace('/ /', '_', $results['Com_Name']); + $comname = preg_replace('/\'/', '', $comname); + $date = $results['Date']; + $filename = "/By_Date/".$date."/".$comname."/".$results['File_Name']; + $filename_shifted = "/By_Date/shifted/".$date."/".$comname."/".$results['File_Name']; + $filename_png = $filename . ".png"; + $sciname = preg_replace('/ /', '_', $results['Sci_Name']); + $sci_name = $results['Sci_Name']; + $time = $results['Time']; + $confidence = round((float)round($results['Confidence'],2) * 100 ) . '%'; + $filename_formatted = $date."/".$comname."/".$results['File_Name']; + + // file was deleted by disk check, no need to show the detection in recordings + if(!file_exists($home."/BirdSongs/Extracted/".$filename)) { + continue; + } + if(!in_array($filename_formatted, $disk_check_exclude_arr) && isset($_GET['only_excluded'])) { + continue; + } + $iter++; + + if($num_rows < 100){ + $imageelem = ""; + } else { + $imageelem = ""; + } + + if($config["FULL_DISK"] == "purge") { + if(!in_array($filename_formatted, $disk_check_exclude_arr)) { + $imageicon = "images/unlock.svg"; + $title = "This file will be deleted when disk space needs to be freed (>95% usage)."; + $type = "add"; + } else { + $imageicon = "images/lock.svg"; + $title = "This file is excluded from being purged."; + $type = "del"; + } + + if(file_exists($shifted_path.$filename_formatted)) { + $shiftImageIcon = "images/unshift.svg"; + $shiftTitle = "This file has been shifted down in frequency."; + $shiftAction = "unshift"; + $filename = $filename_shifted; + } else { + $shiftImageIcon = "images/shift.svg"; + $shiftTitle = "This file is not shifted in frequency."; + $shiftAction = "shift"; + } + + echo " + + "; + } else { + echo " + + "; + } + + }if($iter == 0){ echo "";}echo "
$name
+ + + + + $date $time
$confidence
+ + ".$imageelem." +
$date $time
$confidence +
+ ".$imageelem." +
No recordings were found.

They may have been deleted to make space for new recordings. You can prevent this from happening in the future by clicking the icon in the top right of a recording.
You can also modify this behavior globally under \"Full Disk Behavior\" here.
";} + + if(isset($_GET['filename'])){ + $name = $_GET['filename']; + $statement2 = $db->prepare("SELECT * FROM detections where File_name == \"$name\" ORDER BY Date DESC, Time DESC"); + ensure_db_ok($statement2); + $result2 = $statement2->execute(); + echo " + + + "; + while($results=$result2->fetchArray(SQLITE3_ASSOC)) + { + $comname = preg_replace('/ /', '_', $results['Com_Name']); + $comname = preg_replace('/\'/', '', $comname); + $date = $results['Date']; + $filename = "/By_Date/".$date."/".$comname."/".$results['File_Name']; + $filename_shifted = "/By_Date/shifted/".$date."/".$comname."/".$results['File_Name']; + $filename_png = $filename . ".png"; + $sciname = preg_replace('/ /', '_', $results['Sci_Name']); + $sci_name = $results['Sci_Name']; + $time = $results['Time']; + $confidence = round((float)round($results['Confidence'],2) * 100 ) . '%'; + $filename_formatted = $date."/".$comname."/".$results['File_Name']; + + // add disk_check_exclude.txt lines into an array for grepping + $fp = @fopen($home."/BirdNET-Pi/scripts/disk_check_exclude.txt", 'r'); + if ($fp) { + $disk_check_exclude_arr = explode("\n", fread($fp, filesize($home."/BirdNET-Pi/scripts/disk_check_exclude.txt"))); + } else { + $disk_check_exclude_arr = []; + } + + if($config["FULL_DISK"] == "purge") { + if(!in_array($filename_formatted, $disk_check_exclude_arr)) { + $imageicon = "images/unlock.svg"; + $title = "This file will be deleted when disk space needs to be freed (>95% usage)."; + $type = "add"; + } else { + $imageicon = "images/lock.svg"; + $title = "This file is excluded from being purged."; + $type = "del"; + } + + if(file_exists($shifted_path.$filename_formatted)) { + $shiftImageIcon = "images/unshift.svg"; + $shiftTitle = "This file has been shifted down in frequency."; + $shiftAction = "unshift"; + $filename = $filename_shifted; + } else { + $shiftImageIcon = "images/shift.svg"; + $shiftTitle = "This file is not shifted in frequency."; + $shiftAction = "shift"; + } + + echo " + + "; + } else { + echo " + + "; + } + + }echo "
$name
+ + + + +$date $time
$confidence
+ +
$date $time
$confidence +
+
";} + echo "
"; +if (get_included_files()[0] === __FILE__) { + echo ''; +} diff --git a/birdnet-pi/rootfs/helpers/change_id/style.css b/birdnet-pi/rootfs/helpers/change_id/style.css new file mode 100644 index 000000000..3274e2296 --- /dev/null +++ b/birdnet-pi/rootfs/helpers/change_id/style.css @@ -0,0 +1,894 @@ +@font-face { + font-family: 'Roboto Flex' ; + src: url('static/RobotoFlex-Regular.ttf') format('truetype'); +} + +* { + font-family: 'Roboto Flex', sans-serif; + box-sizing: border-box; + font-size: medium; +} + +a { + text-decoration: none; +} + +a:hover { + font-weight: bold; +} + +h3 { + text-align: center; + margin-bottom: 12px; +} + +iframe { + border: none; + height: 85%; + width: 100%; + position: fixed; +} + +body { + margin: 0; + background-color: rgb(119, 196, 135); +} + +table { + background-color: transparent; + border-collapse: collapse; + border-spacing: 0; + margin-left: auto; + margin-right: auto; + box-shadow: 0px 0px 17px 1px rgba(0, 0, 0, 0.10); + border-radius:3px; + overflow: hidden; +} + +td { + padding: 10px; + vertical-align: top; + background-color: rgb(219, 255, 235); + font-weight: lighter; + text-align: center; +} + +th { + padding: 12px; + font-weight: bold; + height: auto; + text-align: center; +} + +audio, video{ + max-height: 100%; + max-width: 100%; +} + +label { + font-weight: bold; +} + +hr { + border-color:black; +} + +button { + background-color: transparent; + border: none; + color: black; + cursor: pointer; + transition:background-color 0.2s; +} + +.disabled{ + cursor: not-allowed; + pointer-events: none; + opacity:0.5; +} + +button:hover { + color: blue; +} + +.row { + display: flex; +} + +.centered { + text-align: center; + display: block; + width: auto; + margin-left: auto; + margin-right: auto; +} + +.banner { + height: 7%; + text-align: center; +} + +.banner h1 { + padding: .5em; + letter-spacing: 5px; +} + +.banner audio,.banner form { + float: right; + width: 120px; + margin-left: -120px; + margin-right: auto; +} + +.banner button { + padding-top: 8px; + font-weight: bold; + letter-spacing: 1px; +} + +.banner a { + text-decoration: none; + color: black; + font-size: x-large; +} + +.logo img { + position: absolute; + top: 0; + left: 0; + padding: 10px; +} + +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0,0,0,0.4); +} + +.modal-content { + background-color: #fefefe; + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + width: 80%; + text-align: center; /* Center the content */ +} + +@media (max-width: 768px) { + .modal-content { + width: 95%; + margin: 10% auto; + } + #labelDropdown { + max-width: 100%; + overflow-x: auto; + } +} + +.topnav { + background-color: rgb(159, 226, 155); + display: flex; + flex: 65%; + width: 65%; + min-width: min-content; + justify-content: space-between; + margin-left: auto; + margin-right: auto; + margin-bottom: 15px; + box-shadow: 0px 0px 28px 1px rgba(0, 0, 0, 0.10) !important; + border-radius: 4px; +} + +.topimage { + width:175px; + display:initial !important; +} + +.topnav form { + margin: 0; + padding: 0; +} + +.topnav button { + background-color: transparent; + text-align: center; + padding: 14px 16px; + width: auto; + vertical-align: middle; +} + +.topnav button:hover { + background-color: rgb(219, 255, 235); + color: black; +} + +.topnav button.active { + background-color: #04AA6D; + color: white; +} + +.topnav .button-hover { + background-color: rgb(219, 255, 235) !important; + color: black; +} + +.topnav .icon { + display: none; +} + +.overview th { + background-color: rgb(219, 255, 235); + text-align: center; + padding: 12px; +} + +.overview td { + vertical-align: middle; +} + +.overview div img { + max-height: 100%; + display: flex; + justify-content: center; + margin-right: auto; + margin-left: auto; +} + +.overview .chart { + margin-top: 10px; +} +.overview-stats { + display: flex; + justify-content: center; +} + +.left-column { + flex: 10%; + padding-left: 10px; +} + +.right-column { + flex: 90%; + margin-right: 10%; +} + +.stats td { + vertical-align: middle; +} + +.stats table { + height: auto; +} + +.stats button:hover { + color: blue; +} + +.overview button, .center button{ + font-weight: bold; + color: blue; +} + +.history table,.history img { + width: auto; + margin-left: auto; + margin-right: auto; +} + +.views { + transition: opacity 0.3s; + -webkit-transition: opacity 0.3s; +} + +.views .centered button { + background-color: rgb(219, 255, 235); + padding: 12px; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); +} + +.views .centered button:hover { + background-color: rgb(159, 226, 155); + color: black; + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +.settings { + padding: 12px; +} + +.settings h2 { + margin-top: 0px; +} + +.settings p { + margin-bottom: 0px; +} + +.settings h3 { + text-align: left; +} + +.settings button { + background-color: rgb(219, 255, 235); + padding: 12px; +} + +.settings button:hover { + background-color: rgb(159, 226, 155); + color: black; +} + +.float button { + margin-top: 6px; + margin-bottom: 6px; +} + +.customlabels,.customlabels2 { + float:left; +} + +.customlabels table { + height: 100%; +} + +.customlabels td,.customlabels2 td { + border:none; + background-color: transparent; + vertical-align: middle; +} + +.customlabels button,.customlabels2 button { + padding: 12px; + background-color: rgb(219, 255, 235); + + width: 100%; +} + +.column1, .column3 { + width: 45%; +} + +.smaller { + width: 100%; + display: none; + margin-left: auto; + margin-right: auto; +} + +.column2 { + text-align: center; + width: 10%; + height: 80%; +} + +.column4 { + text-align: justify; + width: 10%; + height: 50%; +} + +.column1 form,.column3 form { + width: 80%; + margin-left: auto; + margin-right: auto; +} + +.column1 select,.column3 select { + height: 80%; + width: 100%; +} + +.spectrogram { + width:50% +} + +.full { + width:100%; +} + +.logbutton, .navbuttons { + float: left; +} + +.systemcontrols form,.servicecontrols form { + /*text-align: center;*/ +} + +.servicecontrols button { + background-color: rgb(219, 255, 235); + padding: 12px; + width: 50%; +} + +.systemcontrols button { + background-color: rgb(219, 255, 235); + display: block; + padding: 12px; + width: 50%; + margin: 16px auto; +} + +.servicecontrols button { + width: 20%; +} + +.btn-group-center { + text-align:center; + /*align-content: center;*/ + margin: 16px auto; + position:relative; + /*display:inline-block;*/ +} + +.slider { + -webkit-appearance: none; + width: 33%; + height: 15px; + border-radius: 5px; + outline: none; + opacity: 0.7; + -webkit-transition: .2s; + transition: opacity .2s; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 25px; + height: 25px; + border-radius: 50%; + background: #04AA6D; + cursor: pointer; +} + +.slider::-moz-range-thumb { + width: 25px; + height: 25px; + border-radius: 50%; + background: #04AA6D; + cursor: pointer; +} + +#body::-webkit-scrollbar { + /*display:none*/ +} + +@media screen and (max-width: 1290px) { + .column1,.column2,.column3,.column4 { + height: 90% + } + .left-column { + display: none; + } + .right-column { + flex: 100%; + margin: 0; + } + img { + max-width: 100%; + } + .overview { + overflow-x: hidden; + } + .overview .right-column .chart img { + margin-left: 5%; + margin-right: auto; + margin-top: 10px; + } +} +@media screen and (max-width: 1000px) { + .customlabels form,.customlabels2 form { + width: 95%; + } + .column1, .column3 { + width: 50%; + height: 100%; + } + .column1 select,.column3 select { + height: 70%; + } + .column2,.column4 { + display: none; + } + .smaller{ + display: block; + } + .systemcontrols button,.servicecontrols button { + width: 60%; + padding: 12px; + background-color: rgb(219, 255, 235); + } + .topnav { + flex: 100%; + width: 100%; + } + .topnav button {display: none;} + .topnav button.icon { + padding: 0; + margin: 0; + display: block; + width: 100%; + margin-left: auto; + margin-right: auto; + } + .banner { + height: auto; + } + .banner img { + display: none; + } + .logo img { + display: block; + width: 60px; + height: 60px; + } + .topnav.responsive {position: relative;} + .topnav.responsive button { + display: block; + text-align: center; + } +} + +@media screen and (max-width: 800px) { + .column1, .column3 { + width: 100%; + } + .systemcontrols button,.servicecontrols button { + width: 80%; + padding: 12px; + background-color: rgb(219, 255, 235); + } + .stats img { + width: 100%; + margin-left:auto; + margin-right:auto; + } + .overview img { + width: 100% + } + .banner { + height: auto; + margin-left: 60px; + } + .banner img { + display: none; + } + .stream { + float: right; + display: block; + width: 100px; + } + .logo img { + display: block; + width: 60px; + height: 60px; + } + .play table,.overview table,.stats table { + width: 100%; + } + .topnav { + flex: 100%; + width: 100%; + flex-direction: column; + } + .topnav button { + font-size: large; + width: 100% + } + .topnav button {display: none;} + .topnav button.icon { + margin: 0; + padding: 0; + display: block; + width: 100%; + margin-left: auto; + margin-right: auto; + } + .topnav.responsive {position: relative;} + .topnav.responsive button { + display: block; + } + .left-column { + display: none; + } + .left { + display:none; + } +} + +.copyimage { + position:absolute; + top:7px; + right:7px; + width:25px !important; + height:25px !important; +} + +.copyimage-mobile { + width: 16px !important; + height: 16px !important; +} + +.relative { + position:relative; +} + +.sortbutton { + margin-top:10px; + font-size:x-large; + background:#dbffeb; + padding:5px; + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.10); +} + +button.legacyview { + display: none; + color:gray; + margin:5px; + float:right; + z-index:100; + position:relative; + font-size:small; + background:#dbffeb; + padding:5px; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); +} + +button.legacyview:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +button.loadmore { + margin-top:10px; + font-size:x-large; + background:#dbffeb; + padding:10px; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); + +} + +button.loadmore:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +#searchterm { + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); +} + +#searchterm:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +tr { + background-color:#9fe29b; +} + +.history.centered form { + display:flex; + justify-content: center; +} + +.history.centered input { + margin-right:5px; + border:0px; +} + +.centered form#views button { + box-shadow: 0px 0px 5px 2px rgba(0, 0, 0, 0.10); + margin:2px; + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); +} + +.centered form#views button:hover { + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +dl { + margin: 1em 0 0 1em; +} +dt { + float: left; + clear: left; + width: auto; + text-align: left; + font-weight: bold; + color: black; +} +dd::before { + content: ": "; +} + +input { + box-shadow: 0px 0px 17px 1px rgba(0, 0, 0, 0.10); +} + +dialog { + border:none; +} + +dialog::backdrop { + background: repeating-linear-gradient( + 30deg, + rgba(24, 194, 236, 0.2), + rgba(24, 194, 236, 0.2) 1px, + rgba(24, 194, 236, 0.3) 1px, + rgba(24, 194, 236, 0.3) 20px + ); + backdrop-filter: blur(1px) +} + +.centered_image_container { + font-size:19px !important; + display:inline-block; + position:relative; + margin-bottom:3px; +} + +.centered_image_container img.img1 { + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); + cursor:pointer; + height:95%; + position:absolute; + right:110%; + top:0px; + border-radius: 5px; + width:unset; +} + +.centered_image_container img.img1:hover{ + opacity:0.8; + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +#birdimage { + transition:box-shadow 280ms cubic-bezier(0.4, 0, 0.2, 1); + box-shadow:0px 3px 1px -2px rgb(0 0 0 / 20%), 0px 2px 2px 0px rgb(0 0 0 / 14%), 0px 1px 5px 0px rgb(0 0 0 / 12%); + cursor:pointer; + border-radius: 5px; +} + +#birdimage:hover { + opacity:0.8; + box-shadow:0px 2px 4px -1px rgb(0 0 0 / 20%), 0px 4px 5px 0px rgb(0 0 0 / 14%), 0px 1px 10px 0px rgb(0 0 0 / 12%); +} + +.centered_image_container * { + font-size:19px !important; +} + +.centered_image_container form { + margin-bottom:0px; +} + +.brbanner { + padding:15px; + background-color:rgb(159, 226, 155); + text-align:center; + font-size:large; +} + +#gain.centered { + margin-bottom:10px; +} + +.updatenumber { + margin-left:5px; + position:absolute; + display:inline-block; + background-color:#c8191a; + color:white; + width:20px; + line-height:20px; + border-radius:12px; + text-align:center; + font-size:small; +} + +form#views button .updatenumber { + position:initial; + margin-left:0px; +} + +#detections_table_overview table { + width:944px; +} + +#recent_detection_middle_td{ + width:33%; +} +@media screen and (max-width:500px) { + #recent_detection_middle_td{ + width:66%; + } +} +#recent_detection_middle_td img{ + width:unset !important; + height:75px; + float:left; +} + +.settingstable { + margin-left:unset; + margin-right:unset; +} +.settingstable td { + text-align:unset; +} +.settingstable textarea { + width:100%; + margin-top:10px; +} +.settingstable h2 { + font-size:x-large; +} +.plaintable { + box-shadow: unset; +} +.plaintable td { + padding: unset; +} + +.brbanner h1 { + margin:0px; + font-size: xx-large; +} + +.testbtn { + background:#77c487 !important; +} + +pre.bash { + background-color: black; + color: white; + font-size: medium ; + font-family: Consolas,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New, monospace; + width: 100%; + display: inline-block; +} +pre#timer.bash { + display:unset; + width:unset; +} + +#toolsbtn { + min-width: max-content; +} + +#showpassword { + cursor:pointer; + margin-left:2px; + height:5px; + line-height:5px; + padding:3px; + background-color:#9fe29b +} + +#newrtspstream{ + cursor: pointer; + margin-left: 2px; + height: 5px; + line-height: 5px; + padding: 3px; + background-color: #9fe29b; +} + +.exclude_species_list_option_highlight { + color: black; + background-color: rgb(119, 196, 135); + font-weight: bolder; +} + +#ddnewline::before { + content: none; +} diff --git a/birdnet-pi/rootfs/helpers/convert_list.php b/birdnet-pi/rootfs/helpers/convert_list/convert_list.php similarity index 56% rename from birdnet-pi/rootfs/helpers/convert_list.php rename to birdnet-pi/rootfs/helpers/convert_list/convert_list.php index 8ab072634..6e4bee985 100644 --- a/birdnet-pi/rootfs/helpers/convert_list.php +++ b/birdnet-pi/rootfs/helpers/convert_list/convert_list.php @@ -2,27 +2,33 @@ +

This tool will allow to convert on-the-fly species to compensate for model errors. It SHOULD NOT BE USED except if you know what you are doing, instead the model errors should be reported to the owner. However, it is still convenient for systematic biases that are confirmed through careful listening of samples, while waiting for the models to be updated.

+

Specie to convert from :

+ +

Specie to convert to :

+ + @@ -49,7 +55,7 @@ $filename = './scripts/convert_species_list.txt'; // Changed the file path $eachline = file($filename, FILE_IGNORE_NEW_LINES); foreach($eachline as $lines){ - echo + echo ""; }?> @@ -66,14 +72,30 @@ document.getElementById("add").addEventListener("submit", function(event) { var speciesSelect1 = document.getElementById("species1"); var speciesSelect2 = document.getElementById("species2"); - var selectedSpecies1 = speciesSelect1.options[speciesSelect1.selectedIndex].value; - var selectedSpecies2 = speciesSelect2.options[speciesSelect2.selectedIndex].value; - document.getElementById("species").value = selectedSpecies1 + ";" + selectedSpecies2; - if (speciesSelect1.selectedIndex < 1 || speciesSelect2.selectedIndex < 1) { + if (speciesSelect1.selectedIndex < 0 || speciesSelect2.selectedIndex < 0) { alert("Please select a species from both lists."); document.querySelector('.views').style.opacity = 1; event.preventDefault(); + } else { + var selectedSpecies1 = speciesSelect1.options[speciesSelect1.selectedIndex].value; + var selectedSpecies2 = speciesSelect2.options[speciesSelect2.selectedIndex].value; + document.getElementById("species").value = selectedSpecies1 + ";" + selectedSpecies2; } }); - + + // Function to filter options in a select element + function filterOptions(id) { + var input = document.getElementById(id + "Search"); + var filter = input.value.toUpperCase(); + var select = document.getElementById(id); + var options = select.getElementsByTagName("option"); + for (var i = 0; i < options.length; i++) { + var txtValue = options[i].textContent || options[i].innerText; + if (txtValue.toUpperCase().indexOf(filter) > -1) { + options[i].style.display = ""; + } else { + options[i].style.display = "none"; + } + } + } diff --git a/birdnet-pi/rootfs/helpers/convert_list/server.py b/birdnet-pi/rootfs/helpers/convert_list/server.py new file mode 100644 index 000000000..6f2e1d1bb --- /dev/null +++ b/birdnet-pi/rootfs/helpers/convert_list/server.py @@ -0,0 +1,342 @@ +import datetime +import logging +import math +import operator +import os +import time + +import librosa +import numpy as np + +from utils.helpers import get_settings, Detection + +os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3' +os.environ['CUDA_VISIBLE_DEVICES'] = '' + +try: + import tflite_runtime.interpreter as tflite +except BaseException: + from tensorflow import lite as tflite + +log = logging.getLogger(__name__) + + +userDir = os.path.expanduser('~') +INTERPRETER, M_INTERPRETER, INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST = (None, None, None, None, None) +PREDICTED_SPECIES_LIST = [] +model, priv_thresh, sf_thresh = (None, None, None) + +mdata, mdata_params = (None, None) + + +def loadModel(): + + global INPUT_LAYER_INDEX + global OUTPUT_LAYER_INDEX + global MDATA_INPUT_INDEX + global CLASSES + + log.info('LOADING TF LITE MODEL...') + + # Load TFLite model and allocate tensors. + # model will either be BirdNET_GLOBAL_6K_V2.4_Model_FP16 (new) or BirdNET_6K_GLOBAL_MODEL (old) + modelpath = userDir + '/BirdNET-Pi/model/'+model+'.tflite' + myinterpreter = tflite.Interpreter(model_path=modelpath, num_threads=2) + myinterpreter.allocate_tensors() + + # Get input and output tensors. + input_details = myinterpreter.get_input_details() + output_details = myinterpreter.get_output_details() + + # Get input tensor index + INPUT_LAYER_INDEX = input_details[0]['index'] + if model == "BirdNET_6K_GLOBAL_MODEL": + MDATA_INPUT_INDEX = input_details[1]['index'] + OUTPUT_LAYER_INDEX = output_details[0]['index'] + + # Load labels + CLASSES = [] + labelspath = userDir + '/BirdNET-Pi/model/labels.txt' + with open(labelspath, 'r') as lfile: + for line in lfile.readlines(): + CLASSES.append(line.replace('\n', '')) + + log.info('LOADING DONE!') + + return myinterpreter + + +def loadMetaModel(): + + global M_INTERPRETER + global M_INPUT_LAYER_INDEX + global M_OUTPUT_LAYER_INDEX + + if get_settings().getint('DATA_MODEL_VERSION') == 2: + data_model = 'BirdNET_GLOBAL_6K_V2.4_MData_Model_V2_FP16.tflite' + else: + data_model = 'BirdNET_GLOBAL_6K_V2.4_MData_Model_FP16.tflite' + + # Load TFLite model and allocate tensors. + M_INTERPRETER = tflite.Interpreter(model_path=os.path.join(userDir, 'BirdNET-Pi/model', data_model)) + M_INTERPRETER.allocate_tensors() + + # Get input and output tensors. + input_details = M_INTERPRETER.get_input_details() + output_details = M_INTERPRETER.get_output_details() + + # Get input tensor index + M_INPUT_LAYER_INDEX = input_details[0]['index'] + M_OUTPUT_LAYER_INDEX = output_details[0]['index'] + + log.info("loaded META model") + + +def predictFilter(lat, lon, week): + + global M_INTERPRETER + + # Does interpreter exist? + if M_INTERPRETER is None: + loadMetaModel() + + # Prepare mdata as sample + sample = np.expand_dims(np.array([lat, lon, week], dtype='float32'), 0) + + # Run inference + M_INTERPRETER.set_tensor(M_INPUT_LAYER_INDEX, sample) + M_INTERPRETER.invoke() + + return M_INTERPRETER.get_tensor(M_OUTPUT_LAYER_INDEX)[0] + + +def explore(lat, lon, week): + + # Make filter prediction + l_filter = predictFilter(lat, lon, week) + + # Apply threshold + l_filter = np.where(l_filter >= float(sf_thresh), l_filter, 0) + + # Zip with labels + l_filter = list(zip(l_filter, CLASSES)) + + # Sort by filter value + l_filter = sorted(l_filter, key=lambda x: x[0], reverse=True) + + return l_filter + + +def predictSpeciesList(lat, lon, week): + + l_filter = explore(lat, lon, week) + for s in l_filter: + if s[0] >= float(sf_thresh): + # if there's a custom user-made include list, we only want to use the species in that + if (len(INCLUDE_LIST) == 0): + PREDICTED_SPECIES_LIST.append(s[1]) + + +def loadCustomSpeciesList(path): + + slist = [] + if os.path.isfile(path): + with open(path, 'r') as csfile: + for line in csfile.readlines(): + slist.append(line.replace('\r', '').replace('\n', '')) + + return slist + + +def splitSignal(sig, rate, overlap, seconds=3.0, minlen=1.5): + + # Split signal with overlap + sig_splits = [] + for i in range(0, len(sig), int((seconds - overlap) * rate)): + split = sig[i:i + int(seconds * rate)] + + # End of signal? + if len(split) < int(minlen * rate): + break + + # Signal chunk too short? Fill with zeros. + if len(split) < int(rate * seconds): + temp = np.zeros((int(rate * seconds))) + temp[:len(split)] = split + split = temp + + sig_splits.append(split) + + return sig_splits + + +def readAudioData(path, overlap, sample_rate=48000): + + log.info('READING AUDIO DATA...') + + # Open file with librosa (uses ffmpeg or libav) + sig, rate = librosa.load(path, sr=sample_rate, mono=True, res_type='kaiser_fast') + + # Split audio into 3-second chunks + chunks = splitSignal(sig, rate, overlap) + + log.info('READING DONE! READ %d CHUNKS.', len(chunks)) + + return chunks + + +def convertMetadata(m): + + # Convert week to cosine + if m[2] >= 1 and m[2] <= 48: + m[2] = math.cos(math.radians(m[2] * 7.5)) + 1 + else: + m[2] = -1 + + # Add binary mask + mask = np.ones((3,)) + if m[0] == -1 or m[1] == -1: + mask = np.zeros((3,)) + if m[2] == -1: + mask[2] = 0.0 + + return np.concatenate([m, mask]) + + +def custom_sigmoid(x, sensitivity=1.0): + return 1 / (1.0 + np.exp(-sensitivity * x)) + + +def predict(sample, sensitivity): + global INTERPRETER + # Make a prediction + INTERPRETER.set_tensor(INPUT_LAYER_INDEX, np.array(sample[0], dtype='float32')) + if model == "BirdNET_6K_GLOBAL_MODEL": + INTERPRETER.set_tensor(MDATA_INPUT_INDEX, np.array(sample[1], dtype='float32')) + INTERPRETER.invoke() + prediction = INTERPRETER.get_tensor(OUTPUT_LAYER_INDEX)[0] + + # Apply custom sigmoid + p_sigmoid = custom_sigmoid(prediction, sensitivity) + + # Get label and scores for pooled predictions + p_labels = dict(zip(CLASSES, p_sigmoid)) + + # Sort by score + p_sorted = sorted(p_labels.items(), key=operator.itemgetter(1), reverse=True) + + human_cutoff = max(10, int(len(p_sorted) * priv_thresh / 100.0)) + + log.debug("DATABASE SIZE: %d", len(p_sorted)) + log.debug("HUMAN-CUTOFF AT: %d", human_cutoff) + + for i in range(min(10, len(p_sorted))): + if p_sorted[i][0] == 'Human_Human': + with open(userDir + '/BirdNET-Pi/HUMAN.txt', 'a') as rfile: + rfile.write(str(datetime.datetime.now()) + str(p_sorted[i]) + ' ' + str(human_cutoff) + '\n') + + return p_sorted[:human_cutoff] + + +def analyzeAudioData(chunks, lat, lon, week, sens, overlap,): + global INTERPRETER + + sensitivity = max(0.5, min(1.0 - (sens - 1.0), 1.5)) + + detections = {} + start = time.time() + log.info('ANALYZING AUDIO...') + + if model == "BirdNET_GLOBAL_6K_V2.4_Model_FP16": + if len(PREDICTED_SPECIES_LIST) == 0 or len(INCLUDE_LIST) != 0: + predictSpeciesList(lat, lon, week) + + mdata = get_metadata(lat, lon, week) + + # Parse every chunk + pred_start = 0.0 + for c in chunks: + + # Prepare as input signal + sig = np.expand_dims(c, 0) + + # Make prediction + p = predict([sig, mdata], sensitivity) +# print("PPPPP",p) + HUMAN_DETECTED = False + + # Catch if Human is recognized + for x in range(len(p)): + if "Human" in p[x][0]: + HUMAN_DETECTED = True + + # Save result and timestamp + pred_end = pred_start + 3.0 + + # If human detected set all detections to human to make sure voices are not saved + if HUMAN_DETECTED is True: + p = [('Human_Human', 0.0)] * 10 + + detections[str(pred_start) + ';' + str(pred_end)] = p + + pred_start = pred_end - overlap + + log.info('DONE! Time %.2f SECONDS', time.time() - start) + return detections + + +def get_metadata(lat, lon, week): + global mdata, mdata_params + if mdata_params != [lat, lon, week]: + mdata_params = [lat, lon, week] + # Convert and prepare metadata + mdata = convertMetadata(np.array([lat, lon, week])) + mdata = np.expand_dims(mdata, 0) + + return mdata + + +def load_global_model(): + global INTERPRETER + global model, priv_thresh, sf_thresh + conf = get_settings() + model = conf['MODEL'] + priv_thresh = conf.getfloat('PRIVACY_THRESHOLD') + sf_thresh = conf.getfloat('SF_THRESH') + INTERPRETER = loadModel() + + +def run_analysis(file): + global INCLUDE_LIST, EXCLUDE_LIST, CONVERT_LIST, CONVERT_DICT + INCLUDE_LIST = loadCustomSpeciesList(os.path.expanduser("~/BirdNET-Pi/include_species_list.txt")) + EXCLUDE_LIST = loadCustomSpeciesList(os.path.expanduser("~/BirdNET-Pi/exclude_species_list.txt")) + CONVERT_LIST = loadCustomSpeciesList(os.path.expanduser("~/BirdNET-Pi/convert_species_list.txt")) + CONVERT_DICT = {row.split(';')[0]: row.split(';')[1] for row in CONVERT_LIST} + + conf = get_settings() + + # Read audio data & handle errors + try: + audio_data = readAudioData(file.file_name, conf.getfloat('OVERLAP')) + except (NameError, TypeError) as e: + log.error("Error with the following info: %s", e) + return [] + + # Process audio data and get detections + raw_detections = analyzeAudioData(audio_data, conf.getfloat('LATITUDE'), conf.getfloat('LONGITUDE'), file.week, + conf.getfloat('SENSITIVITY'), conf.getfloat('OVERLAP')) + confident_detections = [] + for time_slot, entries in raw_detections.items(): + log.info('%s-%s', time_slot, entries[0]) + for entry in entries: + if entry[1] >= conf.getfloat('CONFIDENCE'): + if entry[0] in CONVERT_DICT: + converted_entry = CONVERT_DICT.get(entry[0], entry[0]) + else : + converted_entry = entry[0] + if (converted_entry in INCLUDE_LIST or len(INCLUDE_LIST) == 0) and \ + (converted_entry not in EXCLUDE_LIST or len(EXCLUDE_LIST) == 0) and \ + (converted_entry in PREDICTED_SPECIES_LIST or len(PREDICTED_SPECIES_LIST) == 0): + d = Detection(time_slot.split(';')[0], time_slot.split(';')[1], converted_entry, entry[1]) + confident_detections.append(d) + return confident_detections \ No newline at end of file diff --git a/birdnet-pi/rootfs/helpers/views.php b/birdnet-pi/rootfs/helpers/convert_list/views.php similarity index 100% rename from birdnet-pi/rootfs/helpers/views.php rename to birdnet-pi/rootfs/helpers/convert_list/views.php