diff --git a/index.html b/index.html index 563c4bb6..f184873a 100644 --- a/index.html +++ b/index.html @@ -5,8 +5,8 @@ - - + + diff --git a/js/BirdNet.js b/js/BirdNet.js index 7b522898..b3fed542 100644 --- a/js/BirdNet.js +++ b/js/BirdNet.js @@ -6,7 +6,7 @@ let BACKEND; //GLOBALS let myModel; -const MIGRANTS = new Set(["Pluvialis dominica_American Golden Plover", "Acanthis hornemanni_Arctic Redpoll", "Sterna paradisaea_Arctic Tern", "Recurvirostra avosetta_Avocet", "Porzana pusilla_Baillon's Crake", "Limosa lapponica_Bar-tailed Godwit", "Tyto alba_Barn Owl", "Branta leucopsis_Barnacle Goose", "Cygnus columbianus_Bewick's Swan", "Botaurus stellaris_Bittern", "Chroicocephalus ridibundus_Black-headed Gull", "Podiceps nigricollis_Black-necked Grebe", "Limosa limosa_Black-tailed Godwit", "Turdus merula_Blackbird", "Sylvia atricapilla_Blackcap", "Fringilla montifringilla_Brambling", "Branta bernicla_Brent Goose", "Branta canadensis_Canada Goose", "Larus cachinnans_Caspian Gull", "Phylloscopus collybita_Chiffchaff", "Loxia curvirostra_Common Crossbill", "Larus canus_Common Gull", "Acanthis flammea_Common Redpoll", "Actitis hypoleucos_Common Sandpiper", "Melanitta nigra_Common Scoter", "Sterna hirundo_Common Tern", "Fulica atra_Coot", "Crex crex_Corncrake", "Cuculus canorus_Cuckoo", "Calidris ferruginea_Curlew Sandpiper", "Numenius arquata_Curlew", "Charadrius morinellus_Dotterel", "Calidris alpina_Dunlin", "Prunella modularis_Dunnock", "Alopochen aegyptiaca_Egyptian Goose", "Turdus pilaris_Fieldfare", "Mareca strepera_Gadwall", "Sylvia borin_Garden Warbler", "Spatula querquedula_Garganey", "Regulus regulus_Goldcrest", "Pluvialis apricaria_Golden Plover", "Bucephala clangula_Goldeneye", "Mergus merganser_Goosander", "Locustella naevia_Grasshopper Warbler", "Larus marinus_Great Black-backed Gull", "Podiceps cristatus_Great Crested Grebe", "Tringa ochropus_Green Sandpiper", "Tringa nebularia_Greenshank", "Ardea cinerea_Grey Heron", "Perdix perdix_Grey Partridge", "Phalaropus fulicarius_Grey", "Pluvialis squatarola_Grey Plover", "Motacilla cinerea_Grey Wagtail ", "Anser anser_Greylag Goose", "Delichon urbicum_House Martin", "Coccothraustes coccothraustes_Hawfinch", "Larus argentatus_Herring Gull", "Lymnocryptes minimus_Jack Snipe", "Alcedo atthis_Kingfisher", "Calidris canutus_Knot", "Calcarius lapponicus_Lapland Bunting", "Larus fuscus_Lesser Black-backed Gull", "Acanthis cabaret_Lesser Redpoll ", "Sylvia curruca_Lesser Whitethroat", "Linaria cannabina_Linnet", "Egretta garzetta_Little Egret", "Tachybaptus ruficollis_Little Grebe", "Hydrocoloeus minutus_Little Gull", "Athene noctua_Little Owl", "Charadrius dubius_Little Ringed Plover", "Calidris minuta_Little Stint ", "Sternula albifrons_Little Tern", "Asio otus_Long-eared Owl", "Clangula hyemalis_Long-tailed Duck", "Anas platyrhynchos_Mallard", "Aix galericulata_Mandarin Duck", "Anthus pratensis_Meadow Pipit", "Ichthyaetus melanocephalus_Mediterranean Gull", "Turdus viscivorus_Mistle Thrush", "Gallinula chloropus_Moorhen", "Nycticorax nycticorax_Night Heron", "Luscinia megarhynchos_Nightingale", "Luscinia megarhynchos_Nightingale (song)", "Caprimulgus europaeus_Nightjar", "Anthus hodgsoni_Olive-backed Pipit", "Emberiza hortulana_Ortolan Bunting", "Haematopus ostralegus_Oystercatcher", "Ficedula hypoleuca_Pied Flycatcher", "Motacilla alba_Pied Wagtail", "Anser brachyrhynchus_Pink-footed Goose", "Anas acuta_Pintail", "Aythya ferina_Pochard", "Calidris maritima_Purple Sandpiper", "Coturnix coturnix_Quail", "Mergus serrator_Red-breasted Merganser", "Netta rufina_Red-crested Pochard", "Alectoris rufa_Red-legged Partridge", "Tringa totanus_Redshank", "Phoenicurus phoenicurus_Redstart", "Turdus iliacus_Redwing", "Emberiza schoeniclus_Reed Bunting", "Acrocephalus scirpaceus_Reed Warbler", "Turdus torquatus_Ring Ouzel", "Charadrius hiaticula_Ringed Plover", "Erithacus rubecula_Robin (flight call)", "Anthus petrosus_Rock Pipit", "Sterna dougallii_Roseate Tern", "Calidris pugnax_Ruff", "Riparia riparia_Sand Martin", "Calidris alba_Sanderling", "Thalasseus sandvicensis_Sandwich Tern", "Aythya marila_Scaup", "Loxia scotica_Scottish Crossbill", "Acrocephalus schoenobaenus_Sedge Warbler", "Tadorna tadorna_Shelduck", "Asio flammeus_Short-eared Owl", "Spatula clypeata_Shoveler", "Spinus spinus_Siskin", "Alauda arvensis_Skylark", "Gallinago gallinago_Snipe", "Plectrophenax nivalis_Snow Bunting", "Turdus philomelos_Song Thrush", "Porzana porzana_Spotted Crake", "Muscicapa striata_Spotted Flycatcher", "Tringa erythropus_Spotted Redshank", "Burhinus oedicnemus_Stone-curlew", "Saxicola rubicola_Stonechat", "Hirundo rustica_Swallow", "Apus apus_Swift", "Anser fabalis_Taiga Bean Goose", "Strix aluco_Tawny Owl", "Anas crecca_Teal", "Anthus trivialis_Tree Pipit", "Aythya fuligula_Tufted Duck", "Anser serrirostris_Tundra Bean Goose", "Arenaria interpres_Turnstone", "Anthus spinoletta_Water Pipit", "Rallus aquaticus_Water Rail", "Numenius phaeopus_Whimbrel", "Anser albifrons_White-fronted Goose", "Sylvia communis_Whitethroat", "Cygnus cygnus_Whooper Swan", "Mareca penelope_Wigeon", "Phylloscopus trochilus_Willow Warbler", "Tringa glareola_Wood Sandpiper", "Scolopax rusticola_Woodcock", "Lullula arborea_Woodlark", "Larus michahellis_Yellow-legged Gull", "Motacilla flava_Yellow Wagtail", "Emberiza citrinella_Yellowhammer"]); +const MIGRANTS = new Set(["Pluvialis dominica_American Golden Plover", "Acanthis hornemanni_Arctic Redpoll", "Sterna paradisaea_Arctic Tern", "Recurvirostra avosetta_Avocet", "Porzana pusilla_Baillon's Crake", "Limosa lapponica_Bar-tailed Godwit", "Tyto alba_Barn Owl", "Branta leucopsis_Barnacle Goose", "Cygnus columbianus_Bewick's Swan", "Botaurus stellaris_Bittern", "Chroicocephalus ridibundus_Black-headed Gull", "Podiceps nigricollis_Black-necked Grebe", "Limosa limosa_Black-tailed Godwit", "Turdus merula_Blackbird", "Sylvia atricapilla_Blackcap", "Fringilla montifringilla_Brambling", "Branta bernicla_Brent Goose", "Branta canadensis_Canada Goose", "Larus cachinnans_Caspian Gull", "Phylloscopus collybita_Chiffchaff", "Loxia curvirostra_Common Crossbill", "Larus canus_Common Gull", "Acanthis flammea_Common Redpoll", "Actitis hypoleucos_Common Sandpiper", "Melanitta nigra_Common Scoter", "Sterna hirundo_Common Tern", "Fulica atra_Coot", "Crex crex_Corncrake", "Cuculus canorus_Cuckoo", "Calidris ferruginea_Curlew Sandpiper", "Numenius arquata_Curlew", "Charadrius morinellus_Dotterel", "Calidris alpina_Dunlin", "Prunella modularis_Dunnock", "Alopochen aegyptiaca_Egyptian Goose", "Turdus pilaris_Fieldfare", "Mareca strepera_Gadwall", "Sylvia borin_Garden Warbler", "Spatula querquedula_Garganey", "Regulus regulus_Goldcrest", "Pluvialis apricaria_Golden Plover", "Bucephala clangula_Goldeneye", "Mergus merganser_Goosander", "Locustella naevia_Grasshopper Warbler", "Larus marinus_Great Black-backed Gull", "Podiceps cristatus_Great Crested Grebe", "Tringa ochropus_Green Sandpiper", "Tringa nebularia_Greenshank", "Ardea cinerea_Grey Heron", "Perdix perdix_Grey Partridge", "Phalaropus fulicarius_Grey", "Pluvialis squatarola_Grey Plover", "Motacilla cinerea_Grey Wagtail ", "Anser anser_Greylag Goose", "Delichon urbicum_House Martin", "Coccothraustes coccothraustes_Hawfinch", "Larus argentatus_Herring Gull", "Lymnocryptes minimus_Jack Snipe", "Alcedo atthis_Kingfisher", "Calidris canutus_Knot", "Calcarius lapponicus_Lapland Bunting", "Larus fuscus_Lesser Black-backed Gull", "Acanthis cabaret_Lesser Redpoll ", "Sylvia curruca_Lesser Whitethroat", "Linaria cannabina_Linnet", "Egretta garzetta_Little Egret", "Tachybaptus ruficollis_Little Grebe", "Hydrocoloeus minutus_Little Gull", "Athene noctua_Little Owl", "Charadrius dubius_Little Ringed Plover", "Calidris minuta_Little Stint ", "Sternula albifrons_Little Tern", "Asio otus_Long-eared Owl", "Clangula hyemalis_Long-tailed Duck", "Anas platyrhynchos_Mallard", "Aix galericulata_Mandarin Duck", "Anthus pratensis_Meadow Pipit", "Ichthyaetus melanocephalus_Mediterranean Gull", "Turdus viscivorus_Mistle Thrush", "Gallinula chloropus_Moorhen", "Nycticorax nycticorax_Night Heron", "Luscinia megarhynchos_Nightingale", "Luscinia megarhynchos_Nightingale (song)", "Caprimulgus europaeus_Nightjar", "Anthus hodgsoni_Olive-backed Pipit", "Emberiza hortulana_Ortolan Bunting", "Emberiza pusilla_Little Bunting", "Haematopus ostralegus_Oystercatcher", "Ficedula hypoleuca_Pied Flycatcher", "Motacilla alba_Pied Wagtail", "Anser brachyrhynchus_Pink-footed Goose", "Anas acuta_Pintail", "Aythya ferina_Pochard", "Calidris maritima_Purple Sandpiper", "Coturnix coturnix_Quail", "Mergus serrator_Red-breasted Merganser", "Netta rufina_Red-crested Pochard", "Alectoris rufa_Red-legged Partridge", "Tringa totanus_Redshank", "Phoenicurus phoenicurus_Redstart", "Turdus iliacus_Redwing", "Emberiza schoeniclus_Reed Bunting", "Acrocephalus scirpaceus_Reed Warbler", "Turdus torquatus_Ring Ouzel", "Charadrius hiaticula_Ringed Plover", "Erithacus rubecula_Robin (flight call)", "Anthus petrosus_Rock Pipit", "Sterna dougallii_Roseate Tern", "Calidris pugnax_Ruff", "Riparia riparia_Sand Martin", "Calidris alba_Sanderling", "Thalasseus sandvicensis_Sandwich Tern", "Aythya marila_Scaup", "Loxia scotica_Scottish Crossbill", "Acrocephalus schoenobaenus_Sedge Warbler", "Tadorna tadorna_Shelduck", "Asio flammeus_Short-eared Owl", "Spatula clypeata_Shoveler", "Spinus spinus_Siskin", "Alauda arvensis_Skylark", "Gallinago gallinago_Snipe", "Plectrophenax nivalis_Snow Bunting", "Turdus philomelos_Song Thrush", "Porzana porzana_Spotted Crake", "Muscicapa striata_Spotted Flycatcher", "Tringa erythropus_Spotted Redshank", "Burhinus oedicnemus_Stone-curlew", "Saxicola rubicola_Stonechat", "Hirundo rustica_Swallow", "Apus apus_Swift", "Anser fabalis_Taiga Bean Goose", "Strix aluco_Tawny Owl", "Anas crecca_Teal", "Anthus trivialis_Tree Pipit", "Aythya fuligula_Tufted Duck", "Anser serrirostris_Tundra Bean Goose", "Arenaria interpres_Turnstone", "Anthus spinoletta_Water Pipit", "Rallus aquaticus_Water Rail", "Numenius phaeopus_Whimbrel", "Anser albifrons_White-fronted Goose", "Sylvia communis_Whitethroat", "Cygnus cygnus_Whooper Swan", "Mareca penelope_Wigeon", "Phylloscopus trochilus_Willow Warbler", "Tringa glareola_Wood Sandpiper", "Scolopax rusticola_Woodcock", "Lullula arborea_Woodlark", "Larus michahellis_Yellow-legged Gull", "Motacilla flava_Yellow Wagtail", "Emberiza citrinella_Yellowhammer"]); const NOT_BIRDS = ['Ambient Noise_Ambient Noise', 'Animal_Animal', 'Cat_Cat', 'Church Bells_Church Bells', 'Cough_Cough', 'Dog_Dog', 'Human_Human', 'Laugh_Laugh', 'Rain_Rain', 'Red Fox_Red Fox', 'Sneeze_Sneeze', 'Snoring_Snoring', 'Thunder_Thunder', 'Vehicle_Vehicle', 'Water Drops_Water Drops', 'Waves_Waves', 'Wind_Wind']; const MYSTERIES = ['Unknown Sp._Unknown Sp.']; const GRAYLIST = []; diff --git a/js/model.js b/js/model.js index 846ceef5..6de776ca 100644 --- a/js/model.js +++ b/js/model.js @@ -6,7 +6,7 @@ let BACKEND; //GLOBALS let myModel; -const MIGRANTS = new Set(["Pluvialis dominica_American Golden Plover", "Acanthis hornemanni_Arctic Redpoll", "Sterna paradisaea_Arctic Tern", "Recurvirostra avosetta_Avocet", "Porzana pusilla_Baillon's Crake", "Limosa lapponica_Bar-tailed Godwit", "Tyto alba_Barn Owl", "Branta leucopsis_Barnacle Goose", "Cygnus columbianus_Bewick's Swan", "Botaurus stellaris_Bittern (call)", "Chroicocephalus ridibundus_Black-headed Gull", "Podiceps nigricollis_Black-necked Grebe", "Limosa limosa_Black-tailed Godwit", "Turdus merula_Blackbird (flight call)", "Sylvia atricapilla_Blackcap (call)", "Fringilla montifringilla_Brambling", "Branta bernicla_Brent Goose", "Branta canadensis_Canada Goose", "Larus cachinnans_Caspian Gull", "Phylloscopus collybita_Chiffchaff (call)", "Loxia curvirostra_Common Crossbill", "Larus canus_Common Gull", "Acanthis flammea_Common Redpoll", "Actitis hypoleucos_Common Sandpiper", "Melanitta nigra_Common Scoter", "Sterna hirundo_Common Tern", "Fulica atra_Coot", "Crex crex_Corncrake", "Cuculus canorus_Cuckoo (call)", "Calidris ferruginea_Curlew Sandpiper", "Numenius arquata_Curlew", "Charadrius morinellus_Dotterel", "Calidris alpina_Dunlin", "Prunella modularis_Dunnock (call)", "Alopochen aegyptiaca_Egyptian Goose", "Turdus pilaris_Fieldfare (call)", "Mareca strepera_Gadwall", "Sylvia borin_Garden Warbler (call)", "Spatula querquedula_Garganey", "Regulus regulus_Goldcrest (call)", "Pluvialis apricaria_Golden Plover", "Bucephala clangula_Goldeneye", "Mergus merganser_Goosander", "Locustella naevia_Grasshopper Warbler (call)", "Larus marinus_Great Black-backed Gull", "Podiceps cristatus_Great Crested Grebe", "Tringa ochropus_Green Sandpiper", "Tringa nebularia_Greenshank", "Ardea cinerea_Grey Heron", "Perdix perdix_Grey Partridge", "Phalaropus fulicarius_Grey", "Pluvialis squatarola_Grey Plover", "Motacilla cinerea_Grey Wagtail ", "Anser anser_Greylag Goose", "Delichon urbicum_House Martin", "Coccothraustes coccothraustes_Hawfinch (call)", "Larus argentatus_Herring Gull", "Lymnocryptes minimus_Jack Snipe", "Alcedo atthis_Kingfisher", "Calidris canutus_Knot", "Calcarius lapponicus_Lapland Bunting (call)", "Larus fuscus_Lesser Black-backed Gull", "Acanthis cabaret_Lesser Redpoll ", "Sylvia curruca_Lesser Whitethroat (call)", "Linaria cannabina_Linnet", "Egretta garzetta_Little Egret", "Tachybaptus ruficollis_Little Grebe", "Hydrocoloeus minutus_Little Gull", "Athene noctua_Little Owl", "Charadrius dubius_Little Ringed Plover", "Calidris minuta_Little Stint ", "Sternula albifrons_Little Tern", "Asio otus_Long-eared Owl", "Clangula hyemalis_Long-tailed Duck", "Anas platyrhynchos_Mallard", "Aix galericulata_Mandarin Duck", "Anthus pratensis_Meadow Pipit (call)", "Ichthyaetus melanocephalus_Mediterranean Gull", "Turdus viscivorus_Mistle Thrush (call)", "Gallinula chloropus_Moorhen", "Nycticorax nycticorax_Night Heron", "Luscinia megarhynchos_Nightingale (call)", "Luscinia megarhynchos_Nightingale (song)", "Caprimulgus europaeus_Nightjar (call)", "Anthus hodgsoni_Olive-backed Pipit (call)", "Emberiza hortulana_Ortolan Bunting (call)", "Haematopus ostralegus_Oystercatcher", "Ficedula hypoleuca_Pied Flycatcher (call)", "Motacilla alba_Pied Wagtail", "Anser brachyrhynchus_Pink-footed Goose", "Anas acuta_Pintail", "Aythya ferina_Pochard", "Calidris maritima_Purple Sandpiper", "Coturnix coturnix_Quail (call)", "Mergus serrator_Red-breasted Merganser", "Netta rufina_Red-crested Pochard", "Alectoris rufa_Red-legged Partridge", "Tringa totanus_Redshank", "Phoenicurus phoenicurus_Redstart (call)", "Turdus iliacus_Redwing (call)", "Emberiza schoeniclus_Reed Bunting (call)", "Acrocephalus scirpaceus_Reed Warbler (call)", "Turdus torquatus_Ring Ouzel (call)", "Charadrius hiaticula_Ringed Plover", "Erithacus rubecula_Robin (flight call)", "Anthus petrosus_Rock Pipit", "Sterna dougallii_Roseate Tern", "Calidris pugnax_Ruff", "Riparia riparia_Sand Martin", "Calidris alba_Sanderling", "Thalasseus sandvicensis_Sandwich Tern", "Aythya marila_Scaup", "Loxia scotica_Scottish Crossbill", "Acrocephalus schoenobaenus_Sedge Warbler", "Tadorna tadorna_Shelduck", "Asio flammeus_Short-eared Owl", "Spatula clypeata_Shoveler", "Spinus spinus_Siskin", "Alauda arvensis_Skylark (call)", "Gallinago gallinago_Snipe", "Plectrophenax nivalis_Snow Bunting", "Turdus philomelos_Song Thrush (call)", "Porzana porzana_Spotted Crake", "Muscicapa striata_Spotted Flycatcher", "Tringa erythropus_Spotted Redshank (call)", "Burhinus oedicnemus_Stone-curlew", "Saxicola rubicola_Stonechat", "Hirundo rustica_Swallow", "Apus apus_Swift", "Anser fabalis_Taiga Bean Goose", "Strix aluco_Tawny Owl", "Anas crecca_Teal", "Anthus trivialis_Tree Pipit (call)", "Aythya fuligula_Tufted Duck", "Anser serrirostris_Tundra Bean Goose", "Arenaria interpres_Turnstone", "Anthus spinoletta_Water Pipit", "Rallus aquaticus_Water Rail", "Numenius phaeopus_Whimbrel", "Anser albifrons_White-fronted Goose", "Sylvia communis_Whitethroat (call)", "Cygnus cygnus_Whooper Swan", "Mareca penelope_Wigeon", "Phylloscopus trochilus_Willow Warbler (call)", "Tringa glareola_Wood Sandpiper", "Scolopax rusticola_Woodcock", "Lullula arborea_Woodlark (call)", "Larus michahellis_Yellow-legged Gull", "Motacilla flava_Yellow Wagtail", "Emberiza citrinella_Yellowhammer (call)"]); +const MIGRANTS = new Set(["Pluvialis dominica_American Golden Plover", "Acanthis hornemanni_Arctic Redpoll", "Sterna paradisaea_Arctic Tern", "Recurvirostra avosetta_Avocet", "Porzana pusilla_Baillon's Crake", "Limosa lapponica_Bar-tailed Godwit", "Tyto alba_Barn Owl", "Branta leucopsis_Barnacle Goose", "Cygnus columbianus_Bewick's Swan", "Botaurus stellaris_Bittern (call)", "Chroicocephalus ridibundus_Black-headed Gull", "Podiceps nigricollis_Black-necked Grebe", "Limosa limosa_Black-tailed Godwit", "Turdus merula_Blackbird (flight call)", "Sylvia atricapilla_Blackcap (call)", "Fringilla montifringilla_Brambling", "Branta bernicla_Brent Goose", "Branta canadensis_Canada Goose", "Larus cachinnans_Caspian Gull", "Phylloscopus collybita_Chiffchaff (call)", "Loxia curvirostra_Common Crossbill", "Larus canus_Common Gull", "Acanthis flammea_Common Redpoll", "Actitis hypoleucos_Common Sandpiper", "Melanitta nigra_Common Scoter", "Sterna hirundo_Common Tern", "Fulica atra_Coot", "Crex crex_Corncrake", "Cuculus canorus_Cuckoo (call)", "Calidris ferruginea_Curlew Sandpiper", "Numenius arquata_Curlew", "Charadrius morinellus_Dotterel", "Calidris alpina_Dunlin", "Prunella modularis_Dunnock (call)", "Alopochen aegyptiaca_Egyptian Goose", "Turdus pilaris_Fieldfare (call)", "Mareca strepera_Gadwall", "Sylvia borin_Garden Warbler (call)", "Spatula querquedula_Garganey", "Regulus regulus_Goldcrest (call)", "Pluvialis apricaria_Golden Plover", "Bucephala clangula_Goldeneye", "Mergus merganser_Goosander", "Locustella naevia_Grasshopper Warbler (call)", "Larus marinus_Great Black-backed Gull", "Podiceps cristatus_Great Crested Grebe", "Tringa ochropus_Green Sandpiper", "Tringa nebularia_Greenshank", "Ardea cinerea_Grey Heron", "Perdix perdix_Grey Partridge", "Phalaropus fulicarius_Grey", "Pluvialis squatarola_Grey Plover", "Motacilla cinerea_Grey Wagtail ", "Anser anser_Greylag Goose", "Delichon urbicum_House Martin", "Coccothraustes coccothraustes_Hawfinch (call)", "Larus argentatus_Herring Gull", "Lymnocryptes minimus_Jack Snipe", "Alcedo atthis_Kingfisher", "Calidris canutus_Knot", "Calcarius lapponicus_Lapland Bunting (call)", "Larus fuscus_Lesser Black-backed Gull", "Acanthis cabaret_Lesser Redpoll ", "Sylvia curruca_Lesser Whitethroat (call)", "Linaria cannabina_Linnet", "Egretta garzetta_Little Egret", "Tachybaptus ruficollis_Little Grebe", "Hydrocoloeus minutus_Little Gull", "Athene noctua_Little Owl", "Charadrius dubius_Little Ringed Plover", "Calidris minuta_Little Stint ", "Sternula albifrons_Little Tern", "Asio otus_Long-eared Owl", "Clangula hyemalis_Long-tailed Duck", "Anas platyrhynchos_Mallard", "Aix galericulata_Mandarin Duck", "Anthus pratensis_Meadow Pipit (call)", "Ichthyaetus melanocephalus_Mediterranean Gull", "Turdus viscivorus_Mistle Thrush (call)", "Gallinula chloropus_Moorhen", "Nycticorax nycticorax_Night Heron", "Luscinia megarhynchos_Nightingale (call)", "Luscinia megarhynchos_Nightingale (song)", "Caprimulgus europaeus_Nightjar (call)", "Anthus hodgsoni_Olive-backed Pipit (call)", "Emberiza hortulana_Ortolan Bunting (call)", "Emberiza pusilla_Little Bunting (call)", "Haematopus ostralegus_Oystercatcher", "Ficedula hypoleuca_Pied Flycatcher (call)", "Motacilla alba_Pied Wagtail", "Anser brachyrhynchus_Pink-footed Goose", "Anas acuta_Pintail", "Aythya ferina_Pochard", "Calidris maritima_Purple Sandpiper", "Coturnix coturnix_Quail (call)", "Mergus serrator_Red-breasted Merganser", "Netta rufina_Red-crested Pochard", "Alectoris rufa_Red-legged Partridge", "Tringa totanus_Redshank", "Phoenicurus phoenicurus_Redstart (call)", "Turdus iliacus_Redwing (call)", "Emberiza schoeniclus_Reed Bunting (call)", "Acrocephalus scirpaceus_Reed Warbler (call)", "Turdus torquatus_Ring Ouzel (call)", "Charadrius hiaticula_Ringed Plover", "Erithacus rubecula_Robin (flight call)", "Anthus petrosus_Rock Pipit", "Sterna dougallii_Roseate Tern", "Calidris pugnax_Ruff", "Riparia riparia_Sand Martin", "Calidris alba_Sanderling", "Thalasseus sandvicensis_Sandwich Tern", "Aythya marila_Scaup", "Loxia scotica_Scottish Crossbill", "Acrocephalus schoenobaenus_Sedge Warbler", "Tadorna tadorna_Shelduck", "Asio flammeus_Short-eared Owl", "Spatula clypeata_Shoveler", "Spinus spinus_Siskin", "Alauda arvensis_Skylark (call)", "Gallinago gallinago_Snipe", "Plectrophenax nivalis_Snow Bunting", "Turdus philomelos_Song Thrush (call)", "Porzana porzana_Spotted Crake", "Muscicapa striata_Spotted Flycatcher", "Tringa erythropus_Spotted Redshank (call)", "Burhinus oedicnemus_Stone-curlew", "Saxicola rubicola_Stonechat", "Hirundo rustica_Swallow", "Apus apus_Swift", "Anser fabalis_Taiga Bean Goose", "Strix aluco_Tawny Owl", "Anas crecca_Teal", "Anthus trivialis_Tree Pipit (call)", "Aythya fuligula_Tufted Duck", "Anser serrirostris_Tundra Bean Goose", "Arenaria interpres_Turnstone", "Anthus spinoletta_Water Pipit", "Rallus aquaticus_Water Rail", "Numenius phaeopus_Whimbrel", "Anser albifrons_White-fronted Goose", "Sylvia communis_Whitethroat (call)", "Cygnus cygnus_Whooper Swan", "Mareca penelope_Wigeon", "Phylloscopus trochilus_Willow Warbler (call)", "Tringa glareola_Wood Sandpiper", "Scolopax rusticola_Woodcock", "Lullula arborea_Woodlark (call)", "Larus michahellis_Yellow-legged Gull", "Motacilla flava_Yellow Wagtail", "Emberiza citrinella_Yellowhammer (call)"]); const NOT_BIRDS = ['Ambient Noise_Ambient Noise', 'Animal_Animal', 'Cat_Cat', 'Church Bells_Church Bells', 'Cough_Cough', 'Dog_Dog', 'Human_Human', 'Laugh_Laugh', 'Rain_Rain', 'Red Fox_Red Fox', 'Sneeze_Sneeze', 'Snoring_Snoring', 'Thunder_Thunder', 'Vehicle_Vehicle', 'Water Drops_Water Drops', 'Waves_Waves', 'Wind_Wind']; const MYSTERIES = ['Unknown Sp._Unknown Sp.']; const GRAYLIST = []; diff --git a/js/ui.js b/js/ui.js index d8f18fee..c4086d6e 100644 --- a/js/ui.js +++ b/js/ui.js @@ -1317,6 +1317,7 @@ function updatePrefs() { ///////////////////////// Window Handlers //////////////////////////// let appPath, tempPath; window.onload = async () => { + window.electron.requestWorkerChannel(); contentWrapperElement.addClass('loaded'); // Set config defaults const defaultConfig = { @@ -1329,13 +1330,13 @@ window.onload = async () => { model: 'v2', latitude: 52.87, longitude: 0.89, // Great Snoring :) - location: 'Location not set', + location: 'Great Snoring, North Norfolk, Norfolk, England, United Kingdom', detect: { nocmig: false, contextAware: false, confidence: 45 }, - filters: { active: false, highPassFrequency: 0, lowShelfFrequency: 0, lowShelfAttenuation: -6, SNR: 0 }, + filters: { active: false, highPassFrequency: 0, lowShelfFrequency: 0, lowShelfAttenuation: 18, SNR: 0 }, warmup: true, backend: 'tensorflow', - tensorflow: { threads: diagnostics['Cores'], batchSize: 4 }, - webgl: { threads: 1, batchSize: 4 }, + tensorflow: { threads: diagnostics['Cores'], batchSize: 32 }, + webgl: { threads: 2, batchSize: 32 }, audio: { format: 'mp3', bitrate: 192, quality: 5, downmix: false, padding: false, fade: false }, limit: 500, debug: false @@ -1388,7 +1389,6 @@ window.onload = async () => { colourmap.value = config.colormap; // Nocmig mode state console.log('nocmig mode is ' + config.detect.nocmig); - // Audio preferences: audioFormat.value = config.audio.format; audioBitrate.value = config.audio.bitrate; @@ -1445,6 +1445,7 @@ window.onload = async () => { } ) // establish the message channel + setUpWorkerMessaging() // Set footer year @@ -3682,12 +3683,12 @@ async function createContextMenu(e) { const createOrEdit = (['archive', 'explore'].includes(STATE.mode)) && (region?.attributes.label || target.closest('#summary')) ? 'Edit' : 'Create'; menu.html(` - play_circle Play + play_circle Play search Analyse - - + + post_add ${createOrEdit} Archive Record${plural} @@ -4000,4 +4001,8 @@ window.electron.onDownloadProgress((_event, progressObj) => { updateProgressBar.value = progressObj.percent; if (progressObj.percent > 99) tracking.classList.add('d-none') }); - \ No newline at end of file + +// CI functions +function getFileLoaded() {return fileLoaded}; +function donePredicting() {return !PREDICTING}; +function getAudacityLabels() {return AUDACITY_LABELS[currentFile]}; diff --git a/js/worker.js b/js/worker.js index 8761132a..c4475290 100644 --- a/js/worker.js +++ b/js/worker.js @@ -2119,8 +2119,7 @@ const sendResult = (index, result, fromDBQuery) => { if (typeof result === 'object') { // Convert confidence back to % value result.score = (result.score / 10).toFixed(0) - - // Recreate Audacity labels + // Recreate Audacity labels (will create filtered view of labels if filtered) const audacity = { timestamp: `${result.position}\t${result.position + WINDOW_SIZE}`, cname: result.cname, diff --git a/main.js b/main.js index c6c63852..df491ccb 100644 --- a/main.js +++ b/main.js @@ -249,8 +249,9 @@ async function createWindow() { // Set icon mainWindow.setIcon(__dirname + '/img/icon/icon.png'); - // Hide nav bar - mainWindow.setMenuBarVisibility(false); + // Hide nav bar excpet in ci mode + + mainWindow.setMenuBarVisibility(!!process.env.CI); // and load the index.html of the app. mainWindow.loadFile('index.html'); @@ -331,7 +332,8 @@ app.whenReady().then(async () => { // Read the contents of the file synchronously const fileContent = fs.readFileSync(filePath, 'utf-8'); const config = JSON.parse(fileContent); - DEBUG = config.debug; + DEBUG = process.env.CI === 'e2e' ? false : config.debug; + console.log('CI mode' , process.env.CI) } catch (error) { // Handle errors, for example, file not found @@ -340,32 +342,6 @@ app.whenReady().then(async () => { await createWorker(); await createWindow(); - // We'll be sending one end of this channel to the main world of the - // context-isolated page. - - // We can't use ipcMain.handle() here, because the reply needs to transfer a - // MessagePort. - ipcMain.on('request-worker-channel', (event) => { - // For security reasons, let's make sure only the frames we expect can - // access the worker. - if (event.senderFrame === mainWindow.webContents.mainFrame) { - // Create a new channel ... - const { port1, port2 } = new MessageChannelMain() - // ... send one end to the worker ... - workerWindow.webContents.postMessage('new-client', null, [port1]) - // ... and the other end to the UI window. - event.senderFrame.postMessage('provide-worker-channel', null, [port2]) - // Now the main window and the worker can communicate with each other - // without going through the main process! - } - // Listen for the 'update-variable' message from the renderer process - ipcMain.on('unsaved-records', (_event, data) => { - unsavedRecords = data.newValue; // Update the variable with the new value - console.log('Unsaved records:', unsavedRecords); - }); - }); - - if (process.platform === 'darwin') { //const appIcon = new Tray('./img/icon/icon.png') app.dock.setIcon(__dirname + '/img/icon/icon.png'); @@ -437,6 +413,21 @@ app.on('activate', async () => { } }); +ipcMain.handle('request-worker-channel', async (_event) =>{ + // Create a new channel ... + const { port1, port2 } = new MessageChannelMain() + // ... send one end to the worker ... + workerWindow.webContents.postMessage('new-client', null, [port1]) + // ... and the other end to the UI window. + mainWindow.webContents.postMessage('provide-worker-channel', null, [port2]) + // Now the main window and the worker can communicate with each other + // without going through the main process! +}) + +ipcMain.handle('unsaved-records', (_event, data) => { + unsavedRecords = data.newValue; // Update the variable with the new value + console.log('Unsaved records:', unsavedRecords); +}); ipcMain.handle('openFiles', async (config) => { // Show file dialog to select audio file diff --git a/package.json b/package.json index 7ac9d7f2..627913f7 100644 --- a/package.json +++ b/package.json @@ -5,21 +5,21 @@ "main": "main.js", "scripts": { "start": "electron .", - "export": "electron-builder build -w --x64 ", - "publish": "electron-builder --win -p always", - "minify": "terser js/*.js --output dist", - "postpack": "npm run minify" + "export": "electron-builder build -w --x64", + "publish": "electron-builder --win -p always" }, "repository": { "type": "git", "url": "git+https://github.com/mattk70/Chirpity-Electron.git" }, "build": { - "publish": [{ - "provider": "github", - "owner": "Mattk70", - "repo": "https://github.com/Mattk70/Chirpity-Electron" - }], + "publish": [ + { + "provider": "github", + "owner": "Mattk70", + "repo": "https://github.com/Mattk70/Chirpity-Electron" + } + ], "files": [ "**/*", "!test*${/*}", @@ -45,7 +45,9 @@ "node_modules/ffprobe-static-electron/index.js", "node_modules/ffprobe-static-electron/package.json" ], - "extraResources": ["./Help/example.mp3"], + "extraResources": [ + "./Help/example.mp3" + ], "fileAssociations": [ { "ext": "mp3", @@ -134,7 +136,9 @@ ] }, "win": { - "publish": ["github"], + "publish": [ + "github" + ], "verifyUpdateCodeSignature": false, "asar": true, "icon": "./img/icon/icon.png", @@ -165,30 +169,33 @@ }, "homepage": "https://github.com/mattk70/Chirpity-Electron#readme", "devDependencies": { + "@playwright/test": "^1.39.0", + "@tensorflow/tfjs-converter": "4.10.0", "electron": "^25.3.0", "electron-builder": "24.6.4", - "@tensorflow/tfjs-converter": "4.10.0", - "terser": "5.22.0" + "electron-playwright-helpers": "^1.6.0", + "playwright": "^1.39.0", + "jimp": "0.22.10" }, "dependencies": { "@popperjs/core": "^2.9.2", "@tensorflow/tfjs-node": "4.10.0", + "axios": "1.5.1", "bootstrap": "5.2.2", "browser-id3-writer": "^4.4.0", "colormap": "^2.3.2", + "electron-log": "4.4.8", "electron-settings": "^4.0.2", + "electron-updater": "6.1.4", "fast-png": "^6.1.0", "ffmpeg-static-electron": "^2.0.3", "fluent-ffmpeg": "^2.1.2", "jquery": "3.6.3", "sqlite3": "^5.1.6", "suncalc": "^1.9.0", + "utimes": "^5.1.1", "uuid": "^8.3.2", "wavefile-reader": "^1.1.1", - "wavesurfer.js": "^6.6.3", - "utimes": "^5.1.1", - "electron-updater": "6.1.4", - "electron-log": "4.4.8", - "axios": "1.5.1" + "wavesurfer.js": "^6.6.3" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..154eece2 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,28 @@ +import { defineConfig, PlaywrightTestConfig } from '@playwright/test' + + +export default defineConfig({ + testDir: './test', + maxFailures: 2, + timeout: 10 * 1000, + use: { + // Maximum time each action such as `click()` can take. Defaults to 0 (no limit). + actionTimeout: 0, + + // Name of the browser that runs tests. For example `chromium`, `firefox`, `webkit`. + //browserName: 'chromium', + + // Toggles bypassing Content-Security-Policy. + bypassCSP: true, + + // Channel to use, for example "chrome", "chrome-beta", "msedge", "msedge-beta". + //channel: 'chrome', + + // Run browser in headless mode. + headless: false, + + // Change the default data-testid attribute. + //testIdAttribute: 'pw-test-id', + }, +}); + diff --git a/preload.js b/preload.js index 8663c363..ce7b95db 100644 --- a/preload.js +++ b/preload.js @@ -15,7 +15,7 @@ const windowLoaded = new Promise(resolve => { // We request that the main process sends us a channel we can use to // communicate with the worker. -ipcRenderer.send('request-worker-channel'); + // now see if we have files to load ipcRenderer.send('file-to-load'); @@ -42,12 +42,12 @@ ipcRenderer.once('provide-worker-channel', async (event) => { contextBridge.exposeInMainWorld('electron', { - unsavedRecords: (isTrue) => ipcRenderer.send('unsaved-records', { newValue: isTrue }), + requestWorkerChannel: () => ipcRenderer.invoke('request-worker-channel'), + unsavedRecords: (isTrue) => ipcRenderer.invoke('unsaved-records', { newValue: isTrue }), onDownloadProgress: (callback) => ipcRenderer.on('download-progress', callback), saveFile: (args) => ipcRenderer.invoke('saveFile', args), selectDirectory: () => ipcRenderer.invoke('selectDirectory'), openDialog: (method, config) => ipcRenderer.invoke('openFiles', method, config), - //powerSaveBlocker: (on) => ipcRenderer.invoke('powerSaveBlocker', on), getPath: () => ipcRenderer.invoke('getPath'), getTemp: () => ipcRenderer.invoke('getTemp'), getVersion: () => ipcRenderer.invoke('getVersion'), @@ -63,6 +63,7 @@ contextBridge.exposeInMainWorld('module', { os: os }); +// // Listen for messages from the main process // Function to display update download progress diff --git a/test/menu.spec.ts b/test/menu.spec.ts new file mode 100644 index 00000000..f8355b87 --- /dev/null +++ b/test/menu.spec.ts @@ -0,0 +1,201 @@ +import { _electron as electron } from 'playwright' +import { test, expect } from '@playwright/test' +import { + findLatestBuild, + ipcMainCallFirstListener, + ipcRendererCallFirstListener, + parseElectronApp, + ipcMainInvokeHandler, + ipcRendererInvoke, + ipcRendererSend, + ipcMainEmit, + stubMultipleDialogs +} from 'electron-playwright-helpers' +import { ElectronApplication, Page } from 'playwright' +import jimp from 'jimp' +let electronApp: ElectronApplication +let page: Page +let worker: Page +let example_file: any + +test.beforeAll(async () => { + // find the latest build in the out directory + const latestBuild = findLatestBuild('./dist') + // parse the directory and find paths and other info + const appInfo = parseElectronApp(latestBuild) + // set the CI environment variable to true + process.env.CI = 'e2e' + electronApp = await electron.launch({ + args: [appInfo.main], + executablePath: appInfo.executable + }) + + example_file = await ipcMainInvokeHandler(electronApp, 'getAudio') + console.log('Example file:' , example_file) + await stubMultipleDialogs(electronApp, [ + { + method: 'showOpenDialog', + value: { + filePaths: [example_file], + canceled: false, + }, + }, + { + method: 'showSaveDialog', + value: { + filePath: '/path/to/file', + canceled: false, + }, + }, + ]) + + worker = await electronApp.firstWindow(); + + electronApp.on('window', async (window) => { + const filename = window.url()?.split('/').pop() + console.log(`Window opened: ${filename}`) + page = window + // Wait for the model to be ready + const warmup = page.locator('#warmup') + const slowExpect = expect.configure({ timeout: 15000 }); + await slowExpect(warmup).toHaveClass('dropdown-item text-danger d-none') + // capture errors + page.on('pageerror', (error) => { + console.error(error) + }) + // capture console messages + page.on('console', (msg) => { + console.log(msg.text()) + }) + }) +}) + +test.afterAll(async () => { + //await page.pause() + await electronApp.close() +}) + + +test('Page title is correct', async () => { + page = await electronApp.waitForEvent('window') + const title = await page.title() + expect(title).toBe('Chirpity Bird Call Detection') +}) + + +test(`Analyse works`, async () => { + await page.locator('#navbarDropdown').click() + await page.locator('#open').click() + page.locator('wave').first().waitFor({state: 'visible'}) + await page.locator('#navbarAnalysis').click() + await page.locator('#analyse').click() + await page.locator('#resultTableContainer').waitFor({state: 'visible'}) + const callID = page.locator('#speciesFilter').getByText('Redwing (call)'); + expect(callID).not.toBe(null) +}) + +//test.describe.configure({ mode: 'parallel' }); + +test(`Audacity labels work`, async () => { + // Everything shows 8 rows @ 45% + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByLabel('Show:').selectOption('everything'); + await page.waitForFunction(() => donePredicting()); + const labels = await page.evaluate(() => getAudacityLabels()); + expect(labels.length).toBe(8); + // Migrants shows 2 rows @ 45% + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByLabel('Show:').selectOption('migrants'); + await page.waitForFunction(() => donePredicting()); + const labels2 = await page.evaluate(() => getAudacityLabels()); + expect(labels2.length).toBe(2); + expect(labels2[0].cname).toBe("Redwing (call)") + // reset the list to default + await page.evaluate(() => { + config.list = 'migrants' + }) +}) + +test("Amend file start dialog contains date", async () =>{ + await page.getByRole('button', { name: 'example.mp3' }).click({ + button: 'right' + }); + await page.getByText('edit_calendar Amend File Start Time').click(); + const fileStart = page.locator('#fileStart') + expect(fileStart).toHaveValue('2023-09-23T08:56') + await page.getByRole('button', { name: 'Cancel' }).click(); +}) + +test('Check spectrogram before and after applying filter are different', async () => { + // take a screenshot of the current page + const screenshot1: Buffer = await page.screenshot() + await page.getByRole('button', { name: 'Settings' }).click(); + await page.getByLabel('Low Shelf filter:').fill('1200'); + await page.getByLabel('Attenuation:').fill('18'); + await page.getByText('blur_on').click(); + await page.getByRole('button', { name: 'Settings' }).isHidden(); + await page.waitForFunction(() => getFileLoaded()); + // take a screenshot of the page after filter applied + const screenshot2: Buffer = await page.screenshot() + // compare the two images + const different = jimp.diff(await jimp.read(screenshot1), await jimp.read(screenshot2), 0.001) + expect(different.percent).toBeGreaterThan(0) + // Reset blur + await page.getByText('blur_on').click(); +}) + + +// test('send IPC message from renderer', async () => { +// // evaluate this script in render process +// // requires webPreferences.nodeIntegration true and contextIsolation false +// await page.evaluate(() => { +// // eslint-disable-next-line @typescript-eslint/no-var-requires +// require('electron').ipcRenderer.send('new-window') +// }) +// const newPage = await electronApp.waitForEvent('window') +// expect(newPage).toBeTruthy() +// expect(await newPage.title()).toBe('Window 4') +// page = newPage +// }) + +// test('receive IPC invoke/handle via renderer', async () => { +// // evaluate this script in RENDERER process and collect the result +// const result = await ipcRendererInvoke(page, 'how-many-windows') +// expect(result).toBe(4) +// }) + +// test('receive IPC handle data via main', async () => { +// // evaluate this script in MAIN process and collect the result +// const result = await ipcMainInvokeHandler(electronApp, 'openFiles') +// expect(result).toBe(4) +// }) + +// test('receive synchronous data via ipcRendererCallFirstListener()', async () => { +// const data = await ipcRendererCallFirstListener(page, 'get-synchronous-data') +// expect(data).toBe('Synchronous Data') +// }) + +// test('receive asynchronous data via ipcRendererCallFirstListener()', async () => { +// const data = await ipcRendererCallFirstListener(page, 'get-asynchronous-data') +// expect(data).toBe('Asynchronous Data') +// }) + +// test('receive synchronous data via ipcMainCallFirstListener()', async () => { +// const data = await ipcMainCallFirstListener(electronApp, 'main-synchronous-data') +// expect(data).toBe('Main Synchronous Data') +// }) + +// test('receive asynchronous data via ipcMainCallFirstListener()', async () => { +// const data = await ipcMainCallFirstListener(electronApp, 'main-asynchronous-data') +// expect(data).toBe('Main Asynchronous Data') +// }) + +// test('select a menu item via the main process', async () => { +// await clickMenuItemById(electronApp, 'new-window') +// const newPage = await electronApp.waitForEvent('window') +// expect(newPage).toBeTruthy() +// expect(await newPage.title()).toBe('Window 5') +// page = newPage +// }) + +