diff --git a/Help/keyboard.html b/Help/keyboard.html index a04a9a0e..43263587 100644 --- a/Help/keyboard.html +++ b/Help/keyboard.html @@ -37,6 +37,10 @@ Esc Stop analysis + + Ctrl-Tab + Toggle between exploring the archive and the latest analysis results. This allows you to compare current detections with those you've previously recorded. +
Transport Controls
diff --git a/css/style.css b/css/style.css index eadee84e..a19379fe 100644 --- a/css/style.css +++ b/css/style.css @@ -409,6 +409,8 @@ footer { border-radius: 5px; z-index: 3; display: none; + opacity: 0; + transition: opacity 1.3s ease; /* Smooth transitions for left and right positions */ } /* ============ desktop view .end// ============ */ diff --git a/js/ui.js b/js/ui.js index 2c135262..45ec357d 100644 --- a/js/ui.js +++ b/js/ui.js @@ -53,7 +53,7 @@ window.addEventListener('rejectionhandled', function(event) { config.track && trackEvent(config.UUID, 'Handled UI Promise Rejection', errorMessage, customURLEncode(stackTrace)); }); -const STATE = { +let STATE = { guano: {}, mode: 'analyse', analysisDone: false, @@ -80,7 +80,6 @@ const BATCH_SIZE_LIST = [4, 8, 16, 32, 48, 64, 96]; const fs = window.module.fs; const colormap = window.module.colormap; const p = window.module.p; -const SunCalc = window.module.SunCalc; const uuidv4 = window.module.uuidv4; const os = window.module.os; @@ -153,7 +152,7 @@ window.electron.getVersion() console.log('Error getting app version:', error) }); -let modelReady = false, fileLoaded = false, currentFile; +let modelReady = false, fileLoaded = false; let PREDICTING = false, t0; let region, AUDACITY_LABELS = {}, wavesurfer; let fileStart, bufferStartTime, fileEnd; @@ -655,7 +654,7 @@ function showDatePicker() { const timestamp = new Date(newStart).getTime(); // Send the data to the worker - worker.postMessage({ action: 'update-file-start', file: currentFile, start: timestamp }); + worker.postMessage({ action: 'update-file-start', file: STATE.currentFile, start: timestamp }); trackEvent(config.UUID, 'Settings Change', 'fileStart', newStart); resetResults(); fileStart = timestamp; @@ -717,14 +716,14 @@ function formatAsBootstrapTable(jsonData) { } function showGUANO(){ const icon = document.getElementById('guano'); - if (STATE.guano[currentFile]){ + if (STATE.guano[STATE.currentFile]){ icon.classList.remove('d-none'); icon.setAttribute('data-bs-content', 'New content for the popover'); // Reinitialize the popover to reflect the updated content const popover = bootstrap.Popover.getInstance(icon); popover.setContent({ '.popover-header': 'GUANO Metadata', - '.popover-body': formatAsBootstrapTable(STATE.guano[currentFile]) + '.popover-body': formatAsBootstrapTable(STATE.guano[STATE.currentFile]) }); } else { icon.classList.add('d-none'); @@ -732,8 +731,8 @@ function showGUANO(){ } function renderFilenamePanel() { - if (!currentFile) return; - const openfile = currentFile; + if (!STATE.currentFile) return; + const openfile = STATE.currentFile; const files = STATE.openFiles; showGUANO(); let filenameElement = DOM.filename; @@ -807,7 +806,7 @@ async function generateLocationList(id) { const defaultText = id === 'savedLocations' ? '(Default)' : 'All'; const el = document.getElementById(id); LOCATIONS = undefined; - worker.postMessage({ action: 'get-locations', file: currentFile }); + worker.postMessage({ action: 'get-locations', file: STATE.currentFile }); await waitForLocations(); el.innerHTML = ``; // clear options LOCATIONS.forEach(loc => { @@ -835,7 +834,7 @@ const showLocation = async (fromSelect) => { const customPlaceEl = document.getElementById('customPlace'); const locationSelect = document.getElementById('savedLocations'); // Check if currentfile has a location id - const id = fromSelect ? parseInt(locationSelect.value) : FILE_LOCATION_MAP[currentFile]; + const id = fromSelect ? parseInt(locationSelect.value) : FILE_LOCATION_MAP[STATE.currentFile]; if (id) { newLocation = LOCATIONS.find(obj => obj.id === id); @@ -960,8 +959,10 @@ async function setCustomLocation() { const addLocation = () => { locationID = savedLocationSelect.value; const batch = document.getElementById('batchLocations').checked; - const files = batch ? STATE.openFiles : [currentFile]; + const files = batch ? STATE.openFiles : [STATE.currentFile]; worker.postMessage({ action: 'set-custom-file-location', lat: latEl.value, lon: lonEl.value, place: customPlaceEl.value, files: files }) + generateLocationList('explore-locations'); + locationModal.hide(); } locationAdd.addEventListener('click', addLocation) @@ -1040,7 +1041,7 @@ async function onOpenFiles(args) { * @returns {Promise} */ async function showSaveDialog() { - await window.electron.saveFile({ currentFile: currentFile, labels: AUDACITY_LABELS[currentFile] }); + await window.electron.saveFile({ currentFile: STATE.currentFile, labels: AUDACITY_LABELS[STATE.currentFile] }); } function resetDiagnostics() { @@ -1089,7 +1090,7 @@ const getSelectionResults = (fromDB) => { STATE['selection']['end'] = end.toFixed(3); postAnalyseMessage({ - filesInScope: [currentFile], + filesInScope: [STATE.currentFile], start: STATE['selection']['start'], end: STATE['selection']['end'], offset: 0, @@ -1102,7 +1103,7 @@ function postAnalyseMessage(args) { if (!PREDICTING) { // Start a timer t0_analysis = Date.now(); - disableMenuItem(['analyseSelection']); + disableMenuItem(['analyseSelection', 'explore', 'charts']); const selection = !!args.end; const filesInScope = args.filesInScope; PREDICTING = true; @@ -1149,7 +1150,7 @@ async function fetchLocationAddress(lat, lon) { openStreetMapTimer = setTimeout(async () => { try { if (!LOCATIONS) { - worker.postMessage({ action: 'get-locations', file: currentFile }); + worker.postMessage({ action: 'get-locations', file: STATE.currentFile }); await waitForLocations(); // Ensure this is awaited } const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&zoom=14`); @@ -1274,15 +1275,17 @@ const handleLocationFilterChange = (e) => { function saveAnalyseState() { if (['analyse', 'archive'].includes(STATE.mode)){ + const active = activeRow?.rowIndex -1 // Store a reference to the current file STATE.currentAnalysis = { - file: currentFile, + file: STATE.currentFile, openFiles: STATE.openFiles, mode: STATE.mode, species: isSpeciesViewFiltered(true), offset: STATE.offset, - active: activeRow?.rowIndex -1, - analysisDone: STATE.analysisDone + active: active, + analysisDone: STATE.analysisDone, + sortOrder: STATE.sortOrder } } } @@ -1324,21 +1327,17 @@ async function showExplore() { async function showAnalyse() { disableMenuItem(['active-analysis']); - STATE.diskHasRecords && enableMenuItem(['save2db', 'explore', 'charts']); - STATE.mode = STATE.currentAnalysis.mode - // Tell the worker we are in Analyse/Archive mode + //Restore STATE + STATE = {...STATE, ...STATE.currentAnalysis} worker.postMessage({ action: 'change-mode', mode: STATE.mode }); hideAll(); - currentFile = STATE.currentAnalysis.file; - STATE.openFiles = STATE.currentAnalysis.openFiles; - STATE.analysisDone = STATE.currentAnalysis.analysisDone; - if (currentFile) { + if (STATE.currentFile) { showElement(['spectrogramWrapper'], false); - worker.postMessage({ action: 'update-state', filesToAnalyse: STATE.openFiles}); + worker.postMessage({ action: 'update-state', filesToAnalyse: STATE.openFiles, sortOrder: STATE.sortOrder}); STATE.analysisDone && worker.postMessage({ action: 'filter', - species: STATE.currentAnalysis.species, - offset: STATE.currentAnalysis.offset, - active: STATE.currentAnalysis.active, + species: STATE.species, + offset: STATE.offset, + active: STATE.active, updateSummary: true }); } resetResults(); @@ -1462,7 +1461,7 @@ function adjustSpecDims(redraw, fftSamples, newHeight) { config.specMaxHeight = specHeight; scheduler.postTask(() => updatePrefs('config.json', config), {priority: 'background'}); } - if (currentFile) { + if (STATE.currentFile) { // give the wrapper space for the transport controls and element padding/margins if (!wavesurfer) { initWavesurfer({ @@ -1675,16 +1674,25 @@ function updatePrefs(file, data) { } } -function fillDefaults(config, defaultConfig) { +function syncConfig(config, defaultConfig) { + // First, remove keys from config that are not in defaultConfig + Object.keys(config).forEach(key => { + if (!(key in defaultConfig)) { + delete config[key]; + } + }); + + // Then, fill in missing keys from defaultConfig Object.keys(defaultConfig).forEach(key => { if (!(key in config)) { config[key] = defaultConfig[key]; } else if (typeof config[key] === 'object' && typeof defaultConfig[key] === 'object') { - // Recursively fill in defaults for nested objects - fillDefaults(config[key], defaultConfig[key]); + // Recursively sync nested objects + syncConfig(config[key], defaultConfig[key]); } }); } + ///////////////////////// Window Handlers //////////////////////////// // Set config defaults const defaultConfig = { @@ -1755,7 +1763,7 @@ window.onload = async () => { return false; }; //fill in defaults - after updates add new items - fillDefaults(config, defaultConfig); + syncConfig(config, defaultConfig); // Reset defaults for tensorflow batchsize. If removing, update change handler for batch-size if (config.tensorflow.batchSizeWasReset !== true && config.tensorflow.batchSize === 32) { const RAM = parseInt(DIAGNOSTICS['System Memory'].replace(' GB', '')); @@ -1983,7 +1991,7 @@ const setUpWorkerMessaging = () => { Locate File ` @@ -2013,8 +2021,18 @@ const setUpWorkerMessaging = () => { const mode = args.mode; STATE.mode = mode; renderFilenamePanel(); + switch (mode) { + case 'analyse':{ + STATE.diskHasRecords && !PREDICTING && enableMenuItem(['explore', 'charts']); + break + } + case 'archive':{ + enableMenuItem(['save2db', 'explore', 'charts']); + break + } + } config.debug && console.log("Mode changed to: " + mode); - if (mode === 'archive' || mode === 'explore') { + if (['archive', 'explore'].includes(mode)) { enableMenuItem(['purge-file']); // change header to indicate activation DOM.resultHeader.classList.remove('text-bg-secondary'); @@ -2635,11 +2653,34 @@ function onChartData(args) { tooltip.style.visibility = 'hidden'; }; + // async function specTooltip(event) { + // const waveElement = event.target; + // const specDimensions = waveElement.getBoundingClientRect(); + // const frequencyRange = Number(config.audio.maxFrequency) - Number(config.audio.minFrequency); + // const yPosition = Math.round((specDimensions.bottom - event.clientY) * (frequencyRange / specDimensions.height)) + Number(config.audio.minFrequency); + // tooltip.textContent = `Frequency: ${yPosition}Hz`; + // if (region) { + // const lineBreak = document.createElement('br'); + // const textNode = document.createTextNode(formatRegionTooltip(region.start, region.end)); + + // tooltip.appendChild(lineBreak); // Add the line break + // tooltip.appendChild(textNode); // Add the text node + // } + // Object.assign(tooltip.style, { + // top: `${event.clientY}px`, + // left: `${event.clientX + 15}px`, + // display: 'block', + // visibility: 'visible' + // }); + // } + async function specTooltip(event) { const waveElement = event.target; const specDimensions = waveElement.getBoundingClientRect(); const frequencyRange = Number(config.audio.maxFrequency) - Number(config.audio.minFrequency); const yPosition = Math.round((specDimensions.bottom - event.clientY) * (frequencyRange / specDimensions.height)) + Number(config.audio.minFrequency); + + // Update the tooltip content tooltip.textContent = `Frequency: ${yPosition}Hz`; if (region) { const lineBreak = document.createElement('br'); @@ -2648,15 +2689,32 @@ function onChartData(args) { tooltip.appendChild(lineBreak); // Add the line break tooltip.appendChild(textNode); // Add the text node } + + // Get the tooltip's dimensions + tooltip.style.display = 'block'; // Ensure tooltip is visible to measure dimensions + const tooltipWidth = tooltip.offsetWidth; + const windowWidth = window.innerWidth; + + // Calculate the new tooltip position + let tooltipLeft; + + // If the tooltip would overflow past the right side of the window, position it to the left + if (event.clientX + tooltipWidth + 15 > windowWidth) { + tooltipLeft = event.clientX - tooltipWidth - 5; // Position to the left of the mouse cursor + } else { + tooltipLeft = event.clientX + 15; // Position to the right of the mouse cursor + } + + // Apply styles to the tooltip Object.assign(tooltip.style, { top: `${event.clientY}px`, - left: `${event.clientX + 15}px`, + left: `${tooltipLeft}px`, display: 'block', - visibility: 'visible' + visibility: 'visible', + opacity: 1 }); } - const LIST_MAP = { location: 'Searching for birds in your region', nocturnal: 'Searching for nocturnal birds', @@ -2803,12 +2861,12 @@ function centreSpec(){ const GLOBAL_ACTIONS = { // eslint-disable-line a: function (e) { - if (( e.ctrlKey || e.metaKey) && currentFile) { + if (( e.ctrlKey || e.metaKey) && STATE.currentFile) { const element = e.shiftKey ? 'analyseAll' : 'analyse'; document.getElementById(element).click(); } }, - A: function (e) { ( e.ctrlKey || e.metaKey) && currentFile && document.getElementById('analyseAll').click()}, + A: function (e) { ( e.ctrlKey || e.metaKey) && STATE.currentFile && document.getElementById('analyseAll').click()}, c: function (e) { // Center window on playhead if (( e.ctrlKey || e.metaKey) && currentBuffer) { @@ -2835,7 +2893,7 @@ function centreSpec(){ }, s: function (e) { if ( e.ctrlKey || e.metaKey) { - worker.postMessage({ action: 'save2db', file: currentFile}); + worker.postMessage({ action: 'save2db', file: STATE.currentFile}); } }, t: function (e) { @@ -2863,6 +2921,7 @@ function centreSpec(){ threads: config[config[config.model].backend].threads, list: config.list }); + STATE.diskHasRecords && enableMenuItem(['explore', 'charts']); generateToast({ message:'Operation cancelled'}); DOM.progressDiv.classList.add('d-none'); } @@ -2936,7 +2995,11 @@ function centreSpec(){ '-': function (e) {e.metaKey || e.ctrlKey ? increaseFFT() : zoomSpec('zoomOut')}, ' ': function () { wavesurfer && wavesurfer.playPause() }, Tab: function (e) { - if (activeRow) { + if ((e.metaKey || e.ctrlKey) && ! PREDICTING) { // If you did this when predicting, your results would go straight to the archive + const modeToSet = STATE.mode === 'explore' ? 'active-analysis' : 'explore'; + document.getElementById(modeToSet).click() + } + else if (activeRow) { activeRow.classList.remove('table-active') if (e.shiftKey) { activeRow = activeRow.previousSibling || activeRow; @@ -2962,7 +3025,7 @@ function centreSpec(){ } const postBufferUpdate = ({ - file = currentFile, + file = STATE.currentFile, begin = 0, position = 0, play = false, @@ -2995,7 +3058,7 @@ function centreSpec(){ // Go to position const goto = new bootstrap.Modal(document.getElementById('gotoModal')); const showGoToPosition = () => { - if (currentFile) { + if (STATE.currentFile) { goto.show(); } } @@ -3011,7 +3074,7 @@ function centreSpec(){ const gotoTime = (e) => { - if (currentFile) { + if (STATE.currentFile) { e.preventDefault(); let hours = 0, minutes = 0, seconds = 0; const time = document.getElementById('timeInput').value; @@ -3092,7 +3155,7 @@ function centreSpec(){ clearTimeout(loadingTimeout) // Clear the loading animation DOM.loading.classList.add('d-none'); - const resetSpec = !currentFile; + const resetSpec = !STATE.currentFile; currentFileDuration = sourceDuration; //if (preserveResults) completeDiv.hide(); console.log(`UI received worker-loaded-audio: ${file}, buffered: ${contents === undefined}`); @@ -3110,14 +3173,14 @@ function centreSpec(){ } } else { NEXT_BUFFER = undefined; - if (currentFile !== file) { - currentFile = file; + if (STATE.currentFile !== file) { + STATE.currentFile = file; fileStart = start; fileEnd = new Date(fileStart + (currentFileDuration * 1000)); } - STATE.guano[currentFile] = guano; + STATE.guano[STATE.currentFile] = guano; renderFilenamePanel(); if (config.timeOfDay) { @@ -3259,9 +3322,22 @@ function centreSpec(){ } } +// formatDuration: Used for DIAGNOSTICS Duration +function formatDuration(seconds){ + let duration = ''; + const hours = Math.floor(seconds / 3600); // 1 hour = 3600 seconds + if (hours) duration += `${hours} hours `; + const minutes = Math.floor((seconds % 3600) / 60); // 1 minute = 60 seconds + if (hours || minutes) duration += `${minutes} minutes `; + const remainingSeconds = Math.floor(seconds % 60); // Remaining seconds + duration += `${remainingSeconds} seconds`; + return duration; +} + function onAnalysisComplete({quiet}){ PREDICTING = false; STATE.analysisDone = true; + STATE.diskHasRecords && enableMenuItem(['explore', 'charts']); DOM.progressDiv.classList.add('d-none'); if (quiet) return // DIAGNOSTICS: @@ -3275,7 +3351,7 @@ function centreSpec(){ trackEvent(config.UUID, `${config.model}-${config[config.model].backend}`, 'Analysis Rate', config[config.model].backend, parseInt(rate)); if (! STATE.selection){ - DIAGNOSTICS['Analysis Duration'] = analysisTime + ' seconds'; + DIAGNOSTICS['Analysis Duration'] = formatDuration(analysisTime); DIAGNOSTICS['Analysis Rate'] = rate.toFixed(0) + 'x faster than real time performance.'; generateToast({ message:'Analysis complete.'}); activateResultFilters(); @@ -3301,7 +3377,7 @@ function centreSpec(){ enableMenuItem(['saveLabels', 'saveCSV', 'save-eBird', 'save-Raven']); STATE.mode !== 'explore' && enableMenuItem(['save2db']) } - if (currentFile) enableMenuItem(['analyse']) + if (STATE.currentFile) enableMenuItem(['analyse']) adjustSpecDims(true) } @@ -3387,12 +3463,6 @@ function centreSpec(){ resetResults({clearSummary: false, clearPagination: false, clearResults: false}); } - - // const checkDayNight = (timestamp) => { - // let astro = SunCalc.getTimes(timestamp, config.latitude, config.longitude); - // return (astro.dawn.setMilliseconds(0) < timestamp && astro.dusk.setMilliseconds(0) > timestamp) ? 'daytime' : 'nighttime'; - // } - // TODO: show every detection in the spec window as a region on the spectrogram async function renderResult({ @@ -3451,7 +3521,7 @@ function centreSpec(){ callCount, isDaylight } = result; - const dayNight = isDaylight ? 'daytime' : 'nighttime'; // checkDayNight(timestamp); + const dayNight = isDaylight ? 'daytime' : 'nighttime'; // Todo: move this logic so pre dark sections of file are not even analysed if (config.detect.nocmig && !selection && dayNight === 'daytime') return @@ -3545,7 +3615,7 @@ function centreSpec(){ const [start, end] = position; worker.postMessage({ action: 'delete', - file: currentFile, + file: STATE.currentFile, start: start, end: end, active: getActiveRowID(), @@ -3654,7 +3724,7 @@ function centreSpec(){ if (mode === 'save') { worker.postMessage({ action: 'save', - start: start, file: currentFile, end: end, filename: filename, metadata: metadata + start: start, file: STATE.currentFile, end: end, filename: filename, metadata: metadata }) } else { if (!config.seenThanks) { @@ -3664,7 +3734,7 @@ function centreSpec(){ } worker.postMessage({ action: 'post', - start: start, file: currentFile, end: end, defaultName: filename, metadata: metadata, mode: mode + start: start, file: STATE.currentFile, end: end, defaultName: filename, metadata: metadata, mode: mode }) } } @@ -3933,19 +4003,7 @@ function centreSpec(){ let diagnosticTable = ""; for (let [key, value] of Object.entries(DIAGNOSTICS)) { if (key === 'Audio Duration') { // Format duration as days, hours,minutes, etc. - if (value < 3600) { - value = new Date(value * 1000).toISOString().substring(14, 19); - value = value.replace(':', ' minutes ').concat(' seconds'); - } else if (value < 86400) { - value = new Date(value * 1000).toISOString().substring(11, 19) - value = value.replace(':', ' hours ').replace(':', ' minutes ').concat(' seconds') - } else { - value = new Date(value * 1000).toISOString().substring(8, 19); - const day = parseInt(value.slice(0, 2)) - 1; - const daysString = day === 1 ? '1 day ' : day.toString() + ' days '; - const dateString = daysString + value.slice(3); - value = dateString.replace(':', ' hours ').replace(':', ' minutes ').concat(' seconds'); - } + value = formatDuration(value) } diagnosticTable += ``; } @@ -4460,20 +4518,20 @@ DOM.gain.addEventListener('input', () => { // Records menu case 'save2db': { - worker.postMessage({ action: 'save2db', file: currentFile }); + worker.postMessage({ action: 'save2db', file: STATE.currentFile }); if (config.archive.auto) document.getElementById('compress-and-organise').click(); break } case 'charts': { showCharts(); break } case 'explore': { showExplore(); break } case 'active-analysis': { showAnalyse(); break } case 'compress-and-organise': {compressAndOrganise(); break} - case 'purge-file': { deleteFile(currentFile); break } + case 'purge-file': { deleteFile(STATE.currentFile); break } //Analyse menu - case 'analyse': {postAnalyseMessage({ filesInScope: [currentFile] }); break } + case 'analyse': {postAnalyseMessage({ filesInScope: [STATE.currentFile] }); break } case 'analyseSelection': {getSelectionResults(); break } case 'analyseAll': {postAnalyseMessage({ filesInScope: STATE.openFiles }); break } - case 'reanalyse': {postAnalyseMessage({ filesInScope: [currentFile], reanalyse: true }); break } + case 'reanalyse': {postAnalyseMessage({ filesInScope: [STATE.currentFile], reanalyse: true }); break } case 'reanalyseAll': {postAnalyseMessage({ filesInScope: STATE.openFiles, reanalyse: true }); break } case 'purge-from-toast': { deleteFile(MISSING_FILE); break } @@ -4490,7 +4548,7 @@ DOM.gain.addEventListener('input', () => { updatePrefs('config.json', config) resetResults({clearSummary: true, clearPagination: true, clearResults: true}); setListUIState(config.list); - if (currentFile && STATE.analysisDone) worker.postMessage({ action: 'update-list', list: config.list, refreshResults: true }) + if (STATE.currentFile && STATE.analysisDone) worker.postMessage({ action: 'update-list', list: config.list, refreshResults: true }) break; } @@ -4500,7 +4558,7 @@ DOM.gain.addEventListener('input', () => { case 'settingsHelp': { (async () => await populateHelpModal('Help/settings.html', 'Settings Help'))(); break } case 'usage': { (async () => await populateHelpModal('Help/usage.html', 'Usage Guide'))(); break } case 'bugs': { (async () => await populateHelpModal('Help/bugs.html', 'Join the Chirpity Users community'))(); break } - case 'species': { worker.postMessage({action: 'get-valid-species', file: currentFile}); break } + case 'species': { worker.postMessage({action: 'get-valid-species', file: STATE.currentFile}); break } case 'startTour': { prepTour(); break } case 'eBird': { (async () => await populateHelpModal('Help/ebird.html', 'eBird Record FAQ'))(); break } case 'archive-location-select': { @@ -4767,7 +4825,7 @@ DOM.gain.addEventListener('input', () => { } else { colorMapFieldset.classList.add('d-none') } - if (wavesurfer && currentFile) { + if (wavesurfer && STATE.currentFile) { const fftSamples = wavesurfer.spectrogram.fftSamples; wavesurfer.destroy(); wavesurfer = undefined; @@ -4783,7 +4841,7 @@ DOM.gain.addEventListener('input', () => { const threshold = document.getElementById('color-threshold-slider').valueAsNumber; document.getElementById('color-threshold').textContent = threshold; config.customColormap = {'loud': loud, 'mid': mid, 'quiet': quiet, 'threshold': threshold, 'windowFn': windowFn}; - if (wavesurfer && currentFile) { + if (wavesurfer && STATE.currentFile) { const fftSamples = wavesurfer.spectrogram.fftSamples; wavesurfer.destroy(); wavesurfer = undefined; @@ -4802,7 +4860,7 @@ DOM.gain.addEventListener('input', () => { } case 'spec-labels': { config.specLabels = element.checked; - if (wavesurfer && currentFile) { + if (wavesurfer && STATE.currentFile) { const fftSamples = wavesurfer.spectrogram.fftSamples; wavesurfer.destroy(); wavesurfer = undefined; @@ -5106,7 +5164,7 @@ async function readLabels(labelFile, updating){ const insertManualRecord = (cname, start, end, comment, count, label, action, batch, originalCname, confidence) => { - const files = batch ? STATE.openFiles : currentFile; + const files = batch ? STATE.openFiles : STATE.currentFile; worker.postMessage({ action: 'insert-manual-record', cname: cname, @@ -5164,7 +5222,7 @@ async function readLabels(labelFile, updating){ function deleteFile(file) { // EventHandler caller if (typeof file === 'object' && file instanceof Event) { - file = currentFile; + file = STATE.currentFile; } if (file) { if (confirm(`This will remove ${file} and all the associated detections from the database archive. Proceed?`)) { @@ -5292,7 +5350,7 @@ async function readLabels(labelFile, updating){ // CI functions const getFileLoaded = () => fileLoaded; const donePredicting = () => !PREDICTING; - const getAudacityLabels = () => AUDACITY_LABELS[currentFile]; + const getAudacityLabels = () => AUDACITY_LABELS[STATE.currentFile]; // Update checking for Mac diff --git a/js/worker.js b/js/worker.js index 4cbd2708..3b649205 100644 --- a/js/worker.js +++ b/js/worker.js @@ -374,7 +374,7 @@ async function handleMessage(e) { let {model, batchSize, threads, backend, list} = args; const t0 = Date.now(); STATE.detect.backend = backend; - setGetSummaryQueryInterval() + setGetSummaryQueryInterval(threads) INITIALISED = (async () => { LIST_WORKER = await spawnListWorker(); // this can change the backend if tfjs-node isn't available DEBUG && console.log('List worker took', Date.now() - t0, 'ms to load'); @@ -408,7 +408,7 @@ async function handleMessage(e) { } case 'change-threads': { const threads = e.data.threads; - setGetSummaryQueryInterval() + setGetSummaryQueryInterval(threads) const delta = threads - predictWorkers.length; NUM_WORKERS+=delta; if (delta > 0) { @@ -585,7 +585,6 @@ function savedFileCheck(fileList) { } function setGetSummaryQueryInterval(threads){ - STATE.detect.backend !== 'tensorflow' ? threads * 10 : threads; STATE.incrementor = STATE.detect.backend !== 'tensorflow' ? threads * 10 : threads; } @@ -1078,7 +1077,14 @@ async function locateFile(file) { } } } catch (error) { - console.warn(error.message); // Expected that this happens when the directory doesn't exist + if (error.message.includes('scandir')){ + const match = str.match(/'([^']+)'/); + UI.postMessage({ + event: 'generate-alert', type: 'warning', + message: `Unable to locate folder "${match}". Perhaps the disk was removed?` + }) + } + console.warn(error.message + ' - Disk removed?'); // Expected that this happens when the directory doesn't exist } return null; } @@ -1284,7 +1290,6 @@ function setupCtx(audio, rate, destination, file) { .catch(error => aborted || console.warn(error, file)); }; - function checkBacklog(stream) { return new Promise((resolve) => { const backlog = sumObjectValues(predictionsRequested) - sumObjectValues(predictionsReceived); @@ -1300,7 +1305,28 @@ function checkBacklog(stream) { } }); } - +// function checkBacklog(stream) { +// return new Promise((resolve, reject) => { +// const maxRetries = 20; // Max retries before stopping, to avoid infinite loops +// let retryCount = 0; +// let checkInterval = 50; // Start with 50ms + +// const intervalId = setInterval(() => { +// const backlog = sumObjectValues(predictionsRequested) - sumObjectValues(predictionsReceived); +// DEBUG && console.log('backlog:', backlog); + +// // If backlog is within limits, clear interval and resolve +// if (backlog < predictWorkers.length * 2 || retryCount >= maxRetries) { +// clearInterval(intervalId); +// resolve(stream.read()); +// } else { +// retryCount++; +// // Optionally implement exponential backoff by increasing checkInterval +// checkInterval = Math.min(checkInterval * 2, 1000); // Cap at 1000ms +// } +// }, checkInterval); +// }); +// } /** * @@ -1511,7 +1537,6 @@ const getPredictBuffers = async ({ updateFilesBeingProcessed(file) } DEBUG && console.log('All chunks sent for ', file); - //STREAM.end(); resolve('finished') } else { @@ -1527,9 +1552,9 @@ const getPredictBuffers = async ({ // if we have a full buffer if (concatenatedBuffer.length > highWaterMark) { - const audio_chunk = Buffer.allocUnsafeSlow(highWaterMark); + const audio_chunk = Buffer.allocUnsafe(highWaterMark); concatenatedBuffer.copy(audio_chunk, 0, 0, highWaterMark); - const remainder = Buffer.allocUnsafeSlow(concatenatedBuffer.length - highWaterMark); + const remainder = Buffer.allocUnsafe(concatenatedBuffer.length - highWaterMark); concatenatedBuffer.copy(remainder, 0, highWaterMark); const noHeader = audio_chunk.compare(header, 0, header.length, 0, header.length) const audio = noHeader ? joinBuffers(header, audio_chunk) : audio_chunk; @@ -2254,7 +2279,7 @@ const parsePredictions = async (response) => { DEBUG && console.log('worker being used:', response.worker); if (! STATE.selection) await generateInsertQuery(latestResult, file).catch(error => console.warn('Error generating insert query', error)); let [keysArray, speciesIDBatch, confidenceBatch] = latestResult; - if (index <= 500){ + if (index < 499){ const included = await getIncludedIDs(file).catch( (error) => console.log('Error getting included IDs', error)); for (let i = 0; i < keysArray.length; i++) { let updateUI = false; @@ -2318,7 +2343,9 @@ const parsePredictions = async (response) => { updateFilesBeingProcessed(response.file) DEBUG && console.log(`File ${file} processed after ${(new Date() - predictionStart) / 1000} seconds: ${filesBeingProcessed.length} files to go`); } - !STATE.selection && (STATE.increment() === 0) && getSummary({ interim: true }); + + !STATE.selection && (STATE.increment() === 0) && await getSummary({ interim: true }); + return response.worker } @@ -2521,12 +2548,11 @@ const getSummary = async ({ interim = false, action = undefined, } = {}) => { - const db = STATE.db; const included = STATE.selection ? [] : await getIncludedIDs(); const [sql, params] = prepSummaryStatement(included); const offset = species ? STATE.filteredOffset[species] : STATE.globalOffset; - t0 = Date.now(); + const summary = await STATE.db.allAsync(sql, ...params); const event = interim ? 'update-summary' : 'summary-complete'; UI.postMessage({ @@ -2688,7 +2714,7 @@ const getResults = async ({ } else { species = species || ''; const nocmig = STATE.detect.nocmig ? 'nocturnal' : '' - sendResult(++index, `No ${nocmig} ${species} detections found ${STATE.mode === 'explore' && 'in the Archive'} using the ${STATE.list} list.`, true) + sendResult(++index, `No ${nocmig} ${species} detections found ${STATE.mode === 'explore' ? 'in the Archive' : ''} using the ${STATE.list} list.`, true) } } } diff --git a/package.json b/package.json index d14c70ae..c73e0c26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chirpity", - "version": "2.0.3", + "version": "2.1.0", "description": "Chirpity Nocmig", "main": "main.js", "scripts": {
${key}${value}