ecological-agent-skills 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT_CONTEXT.md +191 -0
- package/CATALOG.md +329 -0
- package/LICENSE +692 -0
- package/README.md +347 -0
- package/bin/install.mjs +168 -0
- package/docs/comparison-with-alternatives.md +38 -0
- package/docs/global-examples-index.md +103 -0
- package/docs/repository-statistics.md +101 -0
- package/docs/theoretical-foundations.md +188 -0
- package/environment.yaml +106 -0
- package/examples/community/arctic_tundra_vegetation_example.md +247 -0
- package/examples/community/bird_landuse_example.md +63 -0
- package/examples/community/phytoplankton_reservoir_example.md +60 -0
- package/examples/community/reef_fish_indopacific_example.md +221 -0
- package/examples/impact/baci_road_example.md +57 -0
- package/examples/impact/ecosystem_services_atlantic_forest.md +83 -0
- package/examples/impact/forest_loss_borneo_timeseries_example.md +225 -0
- package/examples/occupancy/puma_camera_example.md +61 -0
- package/examples/occupancy/snow_leopard_himalayas_example.md +204 -0
- package/examples/reproducible/whittaker_biome_sdm_example.md +406 -0
- package/examples/sdm/anteater_cerrado_example.md +69 -0
- package/examples/sdm/jaguar_amazon_example.md +80 -0
- package/examples/sdm/koala_climate_change_example.md +170 -0
- package/examples/sdm/wolf_recolonization_europe_example.md +193 -0
- package/package.json +43 -0
- package/renv.lock +194 -0
- package/skills/SKILL_INDEX.json +1020 -0
- package/skills/acoustic-monitoring/SKILL.md +163 -0
- package/skills/acoustic-monitoring/examples/example-prompts.md +100 -0
- package/skills/acoustic-monitoring/examples/temperate_forest_birds_example.md +285 -0
- package/skills/acoustic-monitoring/resources/acoustic-indices-reference.md +93 -0
- package/skills/acoustic-monitoring/resources/soundscape-ecology-guide.md +90 -0
- package/skills/acoustic-monitoring/resources/species-id-tools-comparison.md +89 -0
- package/skills/acoustic-monitoring/scripts/batch_species_detection.py +360 -0
- package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.R +235 -0
- package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.py +374 -0
- package/skills/biostatistics-workbench/SKILL.md +140 -0
- package/skills/biostatistics-workbench/examples/example-prompts.md +39 -0
- package/skills/biostatistics-workbench/resources/effect-size-reference.md +81 -0
- package/skills/biostatistics-workbench/resources/glm-family-link-reference.md +47 -0
- package/skills/biostatistics-workbench/resources/test-selection-guide.md +93 -0
- package/skills/biostatistics-workbench/scripts/glm_pipeline.R +78 -0
- package/skills/biostatistics-workbench/scripts/glm_pipeline.py +210 -0
- package/skills/camera-trap-processing/SKILL.md +159 -0
- package/skills/camera-trap-processing/examples/example-prompts.md +103 -0
- package/skills/camera-trap-processing/examples/leopard_serengeti_example.md +231 -0
- package/skills/camera-trap-processing/resources/activity-patterns-reference.md +113 -0
- package/skills/camera-trap-processing/resources/camtrapR-workflow-guide.md +130 -0
- package/skills/camera-trap-processing/resources/detection-event-definition-guide.md +89 -0
- package/skills/camera-trap-processing/scripts/estimate_activity.R +169 -0
- package/skills/camera-trap-processing/scripts/process_camtrap_data.R +179 -0
- package/skills/camera-trap-processing/scripts/process_camtrap_data.py +192 -0
- package/skills/community-ecology-ordination/SKILL.md +133 -0
- package/skills/community-ecology-ordination/examples/example-prompts.md +35 -0
- package/skills/community-ecology-ordination/resources/dissimilarity-metric-guide.md +53 -0
- package/skills/community-ecology-ordination/resources/nmds-interpretation-guide.md +104 -0
- package/skills/community-ecology-ordination/scripts/__pycache__/community_analysis.cpython-311.pyc +0 -0
- package/skills/community-ecology-ordination/scripts/community_analysis.R +143 -0
- package/skills/community-ecology-ordination/scripts/community_analysis.py +231 -0
- package/skills/ecological-data-foundation/SKILL.md +129 -0
- package/skills/ecological-data-foundation/examples/example-prompts.md +40 -0
- package/skills/ecological-data-foundation/resources/coordinate-cleaning-flags.md +66 -0
- package/skills/ecological-data-foundation/resources/darwin-core-glossary.md +91 -0
- package/skills/ecological-data-foundation/resources/data-citation-guide.md +265 -0
- package/skills/ecological-data-foundation/resources/gbif-data-citation-guide.md +193 -0
- package/skills/ecological-data-foundation/resources/qa-checklist.md +83 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/clean_occurrences.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_ebird.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_inat.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_iucn.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/__pycache__/download_from_obis.cpython-311.pyc +0 -0
- package/skills/ecological-data-foundation/scripts/clean_occurrences.R +230 -0
- package/skills/ecological-data-foundation/scripts/clean_occurrences.py +268 -0
- package/skills/ecological-data-foundation/scripts/download_from_ebird.R +251 -0
- package/skills/ecological-data-foundation/scripts/download_from_ebird.py +364 -0
- package/skills/ecological-data-foundation/scripts/download_from_gbif.R +315 -0
- package/skills/ecological-data-foundation/scripts/download_from_gbif.py +407 -0
- package/skills/ecological-data-foundation/scripts/download_from_inat.R +238 -0
- package/skills/ecological-data-foundation/scripts/download_from_inat.py +304 -0
- package/skills/ecological-data-foundation/scripts/download_from_iucn.R +273 -0
- package/skills/ecological-data-foundation/scripts/download_from_iucn.py +344 -0
- package/skills/ecological-data-foundation/scripts/download_from_obis.R +248 -0
- package/skills/ecological-data-foundation/scripts/download_from_obis.py +318 -0
- package/skills/ecological-impact-assessment/SKILL.md +123 -0
- package/skills/ecological-impact-assessment/examples/example-prompts.md +32 -0
- package/skills/ecological-impact-assessment/resources/baci-design-guide.md +55 -0
- package/skills/ecological-impact-assessment/resources/fragmentation-metrics-reference.md +86 -0
- package/skills/ecological-impact-assessment/resources/pressure-index-template.md +78 -0
- package/skills/ecological-impact-assessment/resources/study-design-guide.md +168 -0
- package/skills/ecological-impact-assessment/scripts/baci_analysis.R +161 -0
- package/skills/ecological-impact-assessment/scripts/fragmentation_analysis.py +141 -0
- package/skills/ecological-impact-assessment/scripts/power_analysis_baci.R +274 -0
- package/skills/ecosystem-services-assessment/SKILL.md +125 -0
- package/skills/ecosystem-services-assessment/examples/example-prompts.md +24 -0
- package/skills/ecosystem-services-assessment/resources/es-indicator-reference.md +45 -0
- package/skills/ecosystem-services-assessment/resources/invest-parameter-guide.md +86 -0
- package/skills/ecosystem-services-assessment/resources/rusle-coefficients.md +88 -0
- package/skills/ecosystem-services-assessment/scripts/__pycache__/compute_es.cpython-311.pyc +0 -0
- package/skills/ecosystem-services-assessment/scripts/compute_es.py +189 -0
- package/skills/ecosystem-services-assessment/scripts/tradeoff_analysis.R +161 -0
- package/skills/environmental-time-series/SKILL.md +125 -0
- package/skills/environmental-time-series/examples/example-prompts.md +33 -0
- package/skills/environmental-time-series/resources/anomaly-indices-reference.md +88 -0
- package/skills/environmental-time-series/resources/bfast-parameter-guide.md +69 -0
- package/skills/environmental-time-series/scripts/__pycache__/recovery_trajectory.cpython-311.pyc +0 -0
- package/skills/environmental-time-series/scripts/__pycache__/trend_analysis.cpython-311.pyc +0 -0
- package/skills/environmental-time-series/scripts/recovery_trajectory.R +305 -0
- package/skills/environmental-time-series/scripts/recovery_trajectory.py +178 -0
- package/skills/environmental-time-series/scripts/trend_analysis.R +192 -0
- package/skills/environmental-time-series/scripts/trend_analysis.py +184 -0
- package/skills/geoprocessing-for-ecology/SKILL.md +123 -0
- package/skills/geoprocessing-for-ecology/examples/example-prompts.md +32 -0
- package/skills/geoprocessing-for-ecology/resources/crs-reference.md +62 -0
- package/skills/geoprocessing-for-ecology/resources/global-predictor-sources.md +331 -0
- package/skills/geoprocessing-for-ecology/resources/resampling-methods.md +57 -0
- package/skills/geoprocessing-for-ecology/scripts/__pycache__/download_predictors.cpython-311.pyc +0 -0
- package/skills/geoprocessing-for-ecology/scripts/download_predictors.R +239 -0
- package/skills/geoprocessing-for-ecology/scripts/download_predictors.py +379 -0
- package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.R +224 -0
- package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.py +172 -0
- package/skills/landscape-connectivity/SKILL.md +170 -0
- package/skills/landscape-connectivity/examples/example-prompts.md +96 -0
- package/skills/landscape-connectivity/examples/jaguar_mesoamerica_corridor_example.md +271 -0
- package/skills/landscape-connectivity/resources/circuitscape-parameter-guide.md +155 -0
- package/skills/landscape-connectivity/resources/graph-theory-for-ecology.md +134 -0
- package/skills/landscape-connectivity/resources/resistance-surface-guide.md +141 -0
- package/skills/landscape-connectivity/scripts/connectivity_analysis.py +387 -0
- package/skills/landscape-connectivity/scripts/connectivity_metrics.R +274 -0
- package/skills/landscape-connectivity/scripts/resistance_surface.R +239 -0
- package/skills/model-validation-and-uncertainty/SKILL.md +131 -0
- package/skills/model-validation-and-uncertainty/examples/example-prompts.md +30 -0
- package/skills/model-validation-and-uncertainty/resources/extrapolation-risk-guide.md +236 -0
- package/skills/model-validation-and-uncertainty/resources/metric-selection-guide.md +52 -0
- package/skills/model-validation-and-uncertainty/resources/threshold-selection-guide.md +64 -0
- package/skills/model-validation-and-uncertainty/scripts/__pycache__/validate_model.cpython-311.pyc +0 -0
- package/skills/model-validation-and-uncertainty/scripts/extrapolation_risk.R +315 -0
- package/skills/model-validation-and-uncertainty/scripts/validate_model.py +226 -0
- package/skills/model-validation-and-uncertainty/scripts/validate_sdm.R +162 -0
- package/skills/occupancy-and-detection/SKILL.md +126 -0
- package/skills/occupancy-and-detection/examples/example-prompts.md +33 -0
- package/skills/occupancy-and-detection/resources/detection-history-format.md +100 -0
- package/skills/occupancy-and-detection/resources/occupancy-study-design.md +47 -0
- package/skills/occupancy-and-detection/scripts/__pycache__/occupancy_analysis.cpython-311.pyc +0 -0
- package/skills/occupancy-and-detection/scripts/occupancy_analysis.R +160 -0
- package/skills/occupancy-and-detection/scripts/occupancy_analysis.py +159 -0
- package/skills/population-viability-analysis/SKILL.md +161 -0
- package/skills/population-viability-analysis/examples/african_elephant_pva_example.md +266 -0
- package/skills/population-viability-analysis/examples/example-prompts.md +95 -0
- package/skills/population-viability-analysis/resources/extinction-risk-thresholds.md +128 -0
- package/skills/population-viability-analysis/resources/matrix-model-guide.md +139 -0
- package/skills/population-viability-analysis/resources/sensitivity-elasticity-reference.md +182 -0
- package/skills/population-viability-analysis/scripts/matrix_pva.R +258 -0
- package/skills/population-viability-analysis/scripts/pva_analysis.py +442 -0
- package/skills/population-viability-analysis/scripts/stochastic_pva.R +353 -0
- package/skills/predictive-modeling-best-practices/SKILL.md +136 -0
- package/skills/predictive-modeling-best-practices/examples/example-prompts.md +58 -0
- package/skills/predictive-modeling-best-practices/resources/collinearity-decision-tree.md +65 -0
- package/skills/predictive-modeling-best-practices/resources/sampling-bias-correction.md +267 -0
- package/skills/predictive-modeling-best-practices/resources/spatial-cv-guide.md +73 -0
- package/skills/predictive-modeling-best-practices/scripts/__pycache__/spatial_cv.cpython-311.pyc +0 -0
- package/skills/predictive-modeling-best-practices/scripts/collinearity_check.R +112 -0
- package/skills/predictive-modeling-best-practices/scripts/spatial_cv.py +182 -0
- package/skills/reproducible-ecology-pipeline/SKILL.md +139 -0
- package/skills/reproducible-ecology-pipeline/examples/example-prompts.md +35 -0
- package/skills/reproducible-ecology-pipeline/resources/directory-structure-template.md +94 -0
- package/skills/reproducible-ecology-pipeline/resources/params-yaml-template.yaml +84 -0
- package/skills/reproducible-ecology-pipeline/resources/reproducibility-checklist-template.md +66 -0
- package/skills/reproducible-ecology-pipeline/scripts/generate_file_manifest.py +110 -0
- package/skills/reproducible-ecology-pipeline/scripts/init_project.sh +53 -0
- package/skills/spatial-prioritization/SKILL.md +162 -0
- package/skills/spatial-prioritization/examples/biodiversity_hotspot_prioritization_example.md +289 -0
- package/skills/spatial-prioritization/examples/example-prompts.md +93 -0
- package/skills/spatial-prioritization/resources/cost-surface-reference.md +130 -0
- package/skills/spatial-prioritization/resources/marxan-vs-prioritizr-comparison.md +125 -0
- package/skills/spatial-prioritization/resources/prioritizr-formulation-guide.md +188 -0
- package/skills/spatial-prioritization/resources/representation-targets-guide.md +186 -0
- package/skills/spatial-prioritization/scripts/prioritization_sensitivity.R +320 -0
- package/skills/spatial-prioritization/scripts/run_prioritization.R +336 -0
- package/skills/species-distribution-modeling/SKILL.md +139 -0
- package/skills/species-distribution-modeling/examples/example-prompts.md +36 -0
- package/skills/species-distribution-modeling/resources/algorithm-comparison.md +25 -0
- package/skills/species-distribution-modeling/resources/calibration-area-guide.md +71 -0
- package/skills/species-distribution-modeling/resources/climate-scenario-preparation.md +170 -0
- package/skills/species-distribution-modeling/resources/maxent-calibration-guide.md +211 -0
- package/skills/species-distribution-modeling/resources/sdm-checklist.md +37 -0
- package/skills/species-distribution-modeling/scripts/predict_distribution.R +236 -0
- package/skills/species-distribution-modeling/scripts/predict_distribution.py +286 -0
- package/skills/species-distribution-modeling/scripts/prepare_future_layers.R +351 -0
- package/skills/species-distribution-modeling/scripts/project_scenarios.R +220 -0
- package/skills/species-distribution-modeling/scripts/run_ensemble_sdm.R +99 -0
- package/skills/species-distribution-modeling/scripts/sdm_pipeline.py +318 -0
- package/skills/species-distribution-modeling/scripts/tune_maxnet.R +344 -0
- package/templates/SKILL_TEMPLATE.md +225 -0
- package/templates/checklists/data-submission-checklist.md +38 -0
- package/templates/checklists/post-analysis-checklist.md +55 -0
- package/templates/checklists/pre-analysis-checklist.md +31 -0
- package/templates/prompts/debug-skill.md +47 -0
- package/templates/prompts/invoke-skill.md +34 -0
- package/templates/prompts/invoke-workflow.md +45 -0
- package/templates/reports/technical-report-template.md +80 -0
- package/templates/scripts/logger_setup.R +79 -0
- package/templates/scripts/logger_setup.py +119 -0
- package/templates/scripts/params_loader.R +28 -0
- package/templates/scripts/params_loader.py +38 -0
- package/workflows/analyze-community-structure/WORKFLOW.md +72 -0
- package/workflows/analyze-environmental-change/WORKFLOW.md +73 -0
- package/workflows/assess-ecological-impact/WORKFLOW.md +75 -0
- package/workflows/assess-ecosystem-services/WORKFLOW.md +68 -0
- package/workflows/assess-landscape-connectivity/WORKFLOW.md +84 -0
- package/workflows/build-fire-risk-map/WORKFLOW.md +79 -0
- package/workflows/produce-technical-report/WORKFLOW.md +113 -0
- package/workflows/run-camera-trap-occupancy/WORKFLOW.md +87 -0
- package/workflows/run-conservation-prioritization/WORKFLOW.md +89 -0
- package/workflows/run-multispecies-screening/WORKFLOW.md +197 -0
- package/workflows/run-occupancy-analysis/WORKFLOW.md +74 -0
- package/workflows/run-population-viability/WORKFLOW.md +90 -0
- package/workflows/run-sdm-study/WORKFLOW.md +99 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Soundscape Ecology Guide
|
|
2
|
+
|
|
3
|
+
## The NDSI Framework: Three Components of Soundscape
|
|
4
|
+
|
|
5
|
+
| Component | Definition | Frequency range (typical) | NDSI role |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| Biophony | Sounds produced by biological organisms | 2–8 kHz | Numerator |
|
|
8
|
+
| Geophony | Non-biological natural sounds (wind, rain, rivers) | Broadband | — |
|
|
9
|
+
| Anthrophony | Human-generated sounds (traffic, machinery) | 0.2–2 kHz | Denominator |
|
|
10
|
+
|
|
11
|
+
**NDSI** measures the dominance of biological over anthropogenic signals. Values near +1 indicate pristine soundscapes; values near -1 indicate urbanised or disturbed sites.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Controlling for Diel and Seasonal Variation
|
|
16
|
+
|
|
17
|
+
### Why it matters
|
|
18
|
+
Acoustic diversity peaks at dawn and dusk ("dawn chorus" and "dusk chorus"). Comparing morning recordings from one site with afternoon recordings from another produces spurious differences unrelated to biodiversity.
|
|
19
|
+
|
|
20
|
+
### Recommended control strategies
|
|
21
|
+
|
|
22
|
+
1. **Stratify by time window:** Compare only recordings taken in the same 1-hour window (e.g., 05:00–06:00 for dawn chorus).
|
|
23
|
+
2. **Mixed-effects model with time-of-day as covariate:**
|
|
24
|
+
|
|
25
|
+
```r
|
|
26
|
+
suppressPackageStartupMessages(library(lme4))
|
|
27
|
+
suppressPackageStartupMessages(library(lubridate))
|
|
28
|
+
|
|
29
|
+
indices$hour <- hour(indices$datetime)
|
|
30
|
+
indices$cos_hour <- cos(2 * pi * indices$hour / 24)
|
|
31
|
+
indices$sin_hour <- sin(2 * pi * indices$hour / 24)
|
|
32
|
+
|
|
33
|
+
# Model ACI with hour and site as predictors
|
|
34
|
+
model <- lmer(ACI ~ cos_hour + sin_hour + habitat_type + (1 | site / recorder_id),
|
|
35
|
+
data = indices)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
3. **Seasonal stratification:** Group months into seasons (wet/dry or astronomical seasons) and analyse separately or include as a fixed effect.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Rarefaction for Acoustic Richness Estimation
|
|
43
|
+
|
|
44
|
+
```r
|
|
45
|
+
# Species accumulation curve from BirdNET detections
|
|
46
|
+
# Requires detection matrix (sites × species)
|
|
47
|
+
suppressPackageStartupMessages(library(vegan))
|
|
48
|
+
|
|
49
|
+
det_matrix <- read.csv("detections_filtered.csv") %>%
|
|
50
|
+
group_by(site, species) %>%
|
|
51
|
+
summarise(n = n(), .groups = "drop") %>%
|
|
52
|
+
tidyr::pivot_wider(names_from = species, values_from = n, values_fill = 0)
|
|
53
|
+
|
|
54
|
+
sp_matrix <- as.matrix(det_matrix[, -1])
|
|
55
|
+
sp_accum <- specaccum(sp_matrix, method = "rarefaction")
|
|
56
|
+
plot(sp_accum, xlab = "Recording hours", ylab = "Cumulative species detected")
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Rule:** Flag sites with < 48 hours of recordings as under-sampled relative to asymptotic richness.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Integrating Acoustic and Visual Survey Data
|
|
64
|
+
|
|
65
|
+
| Data type | Acoustic | Camera trap | Transect |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| What it detects | Vocalising species | Mobile species at detection range | Visible species in survey belt |
|
|
68
|
+
| Temporal resolution | Continuous | Event-based | Snapshot |
|
|
69
|
+
| Species ID confidence | Moderate (requires validation) | High (image) | High (observer) |
|
|
70
|
+
| Integration approach | Multi-state model / species accumulation | Occupancy model | Distance sampling |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Common Soundscape Gradients
|
|
75
|
+
|
|
76
|
+
| Gradient | Expected NDSI trend | Expected ACI trend |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Urban → rural | Increasing | Increasing |
|
|
79
|
+
| Deforested → intact forest | Increasing | Increasing |
|
|
80
|
+
| Day → night (tropical) | Variable | Increasing (insect chorus) |
|
|
81
|
+
| Dry → wet season | Increasing | Increasing (amphibians, insects) |
|
|
82
|
+
| Degraded habitat recovery | Increasing over years | Increasing |
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## References
|
|
87
|
+
|
|
88
|
+
- Pijanowski, B.C. et al. (2011). Soundscape ecology: the science of sound in the landscape. *BioScience*, 61(3), 203–216. DOI: 10.1525/bio.2011.61.3.6
|
|
89
|
+
- Sueur, J. & Farina, A. (2015). Ecoacoustics: the ecological investigation and interpretation of environmental sound. *Biosemiotics*, 8(3), 493–502. DOI: 10.1007/s12304-015-9248-x
|
|
90
|
+
- Tucker, D. et al. (2014). Linking ecological condition and the soundscape in fragmented Australian forests. *Landscape Ecology*, 29(4), 745–758. DOI: 10.1007/s10980-014-9978-6
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Species Identification Tools Comparison
|
|
2
|
+
|
|
3
|
+
## Tool Comparison Table
|
|
4
|
+
|
|
5
|
+
| Tool | Taxa covered | Cost | API / CLI | Recommended confidence | Geographic limitation |
|
|
6
|
+
|---|---|---|---|---|---|
|
|
7
|
+
| BirdNET | Birds (10,000+ species) | Free (open source) | CLI + Python API | ≥ 0.7 | Best in North America and Europe |
|
|
8
|
+
| RavenPro | Any (manual + CNN) | Paid ($400–$800) | None (GUI) | User-defined | None |
|
|
9
|
+
| Kaleidoscope Pro | Bats, birds | Paid ($350+) | None (GUI) | ≥ 90% | None |
|
|
10
|
+
| ARBIMON | Tropical birds, frogs | Free (cloud) | Web API | ≥ 0.8 | Tropics focus |
|
|
11
|
+
| Rainforest Connection | Chainsaws, birds | Proprietary | API (partners) | N/A | Tropical forests |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## BirdNET in Detail
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
```bash
|
|
19
|
+
# Install from GitHub
|
|
20
|
+
pip install birdnet
|
|
21
|
+
# Or use the BirdNET-Analyzer standalone
|
|
22
|
+
git clone https://github.com/kahst/BirdNET-Analyzer
|
|
23
|
+
pip install -r requirements.txt
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Command-line usage
|
|
27
|
+
```bash
|
|
28
|
+
python analyze.py \
|
|
29
|
+
--i /path/to/audio/ \
|
|
30
|
+
--o /path/to/output/ \
|
|
31
|
+
--min_conf 0.7 \
|
|
32
|
+
--lat -3.5 \
|
|
33
|
+
--lon -60.2 \
|
|
34
|
+
--week 24 \
|
|
35
|
+
--slist /path/to/species_list.txt
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Parameters
|
|
39
|
+
| Parameter | Description | Default |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| `--min_conf` | Confidence threshold | 0.1 (must increase) |
|
|
42
|
+
| `--lat` / `--lon` | Location for species filtering | None |
|
|
43
|
+
| `--week` | Week of year (1–48) for seasonal filtering | None |
|
|
44
|
+
| `--slist` | Restrict to species in this list | None |
|
|
45
|
+
| `--rtype` | Output format: table, audacity, r, csv | table |
|
|
46
|
+
|
|
47
|
+
### Interpreting Scores
|
|
48
|
+
- < 0.5: Very likely false positive — do not use without validation
|
|
49
|
+
- 0.5–0.7: Possible detection — flag for review
|
|
50
|
+
- 0.7–0.9: Probable detection — use with caution in analyses
|
|
51
|
+
- > 0.9: High confidence — generally reliable for common species
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Validation Protocol
|
|
56
|
+
|
|
57
|
+
To calculate regional precision and recall for BirdNET:
|
|
58
|
+
|
|
59
|
+
1. **Create validation set:** Randomly select 100–200 detections per confidence band (< 0.5, 0.5–0.7, 0.7–0.9, > 0.9).
|
|
60
|
+
2. **Manual verification:** Review spectrograms in Raven or Audacity; mark each as TP or FP.
|
|
61
|
+
3. **Calculate precision per band:**
|
|
62
|
+
|
|
63
|
+
```r
|
|
64
|
+
validation <- data.frame(
|
|
65
|
+
confidence_band = c("<0.5", "0.5-0.7", "0.7-0.9", ">0.9"),
|
|
66
|
+
n_reviewed = c(100, 100, 100, 100),
|
|
67
|
+
n_correct = c(12, 45, 78, 94)
|
|
68
|
+
)
|
|
69
|
+
validation$precision <- validation$n_correct / validation$n_reviewed
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
4. **Apply precision as weight** in species accumulation analyses.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## When Manual Validation Is Mandatory
|
|
77
|
+
|
|
78
|
+
Always validate manually before using BirdNET results for:
|
|
79
|
+
- Species of conservation concern (IUCN threatened categories)
|
|
80
|
+
- First records in a region
|
|
81
|
+
- Rare or vagrant species
|
|
82
|
+
- Any species with < 20 training recordings in BirdNET training set
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## References
|
|
87
|
+
|
|
88
|
+
- Kahl, S. et al. (2021). BirdNET: A deep learning solution for avian diversity monitoring. *Ecological Informatics*, 61, 101236. DOI: 10.1016/j.ecoinf.2021.101236
|
|
89
|
+
- Abrahams, C. (2021). Practical bioacoustics: Passive acoustic monitoring. *British Wildlife* 32(4), 241–249.
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Batch species detection using BirdNET-Analyzer for passive acoustic monitoring.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python batch_species_detection.py <audio_dir> <output_dir>
|
|
9
|
+
[--confidence 0.7] [--lat -3.0] [--lon -60.0]
|
|
10
|
+
[--date 2024-06-01] [--overlap 0.0] [--rtype csv]
|
|
11
|
+
|
|
12
|
+
Requires:
|
|
13
|
+
BirdNET-Analyzer installed and available on PATH as `birdnet_analyzer`
|
|
14
|
+
(or configured via BIRDNET_PATH environment variable).
|
|
15
|
+
|
|
16
|
+
Outputs:
|
|
17
|
+
detections_raw.csv — all detections from all files
|
|
18
|
+
detections_filtered.csv — above confidence threshold
|
|
19
|
+
species_list.csv — unique species with n detections, max conf
|
|
20
|
+
detection_summary.csv — species × hour detection counts
|
|
21
|
+
daily_detection_plot.png — species accumulation and hourly bar chart
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import sys
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
SKILL_NAME = "acoustic-monitoring"
|
|
30
|
+
_LOG_DIR = Path("logs")
|
|
31
|
+
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
_log_file = _LOG_DIR / f"skill_{SKILL_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
|
33
|
+
logging.basicConfig(
|
|
34
|
+
level=logging.INFO,
|
|
35
|
+
format="[%(asctime)s] [%(levelname)s] [" + SKILL_NAME + "] %(message)s",
|
|
36
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
37
|
+
handlers=[
|
|
38
|
+
logging.StreamHandler(sys.stdout),
|
|
39
|
+
logging.FileHandler(_log_file, encoding="utf-8"),
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
logger = logging.getLogger(SKILL_NAME)
|
|
43
|
+
|
|
44
|
+
def log_step(n: int, desc: str) -> None:
|
|
45
|
+
logger.info("-- STEP %d: %s", n, desc)
|
|
46
|
+
|
|
47
|
+
def log_decision(var: str, val, why: str) -> None:
|
|
48
|
+
logger.info("DECISION | %s = %s | %s", var, val, why)
|
|
49
|
+
|
|
50
|
+
import os
|
|
51
|
+
import subprocess
|
|
52
|
+
import csv
|
|
53
|
+
import json
|
|
54
|
+
import re
|
|
55
|
+
from datetime import timedelta
|
|
56
|
+
from collections import defaultdict
|
|
57
|
+
import argparse
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
BIRDNET_CMD = os.environ.get("BIRDNET_PATH", "birdnet_analyzer")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_args():
|
|
64
|
+
parser = argparse.ArgumentParser(
|
|
65
|
+
description="Batch BirdNET species detection for PAM recordings"
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument("audio_dir", help="Directory containing .wav/.flac files")
|
|
68
|
+
parser.add_argument("output_dir", help="Directory for output files")
|
|
69
|
+
parser.add_argument("--confidence", type=float, default=0.7,
|
|
70
|
+
help="Minimum confidence threshold (default: 0.7)")
|
|
71
|
+
parser.add_argument("--lat", type=float, default=None,
|
|
72
|
+
help="Recording latitude (improves species filtering)")
|
|
73
|
+
parser.add_argument("--lon", type=float, default=None,
|
|
74
|
+
help="Recording longitude")
|
|
75
|
+
parser.add_argument("--date", default=None,
|
|
76
|
+
help="Recording date YYYY-MM-DD (enables seasonal filter)")
|
|
77
|
+
parser.add_argument("--overlap", type=float, default=0.0,
|
|
78
|
+
help="Segment overlap in seconds (default: 0.0)")
|
|
79
|
+
parser.add_argument("--rtype", default="csv",
|
|
80
|
+
choices=["csv", "table", "audacity"],
|
|
81
|
+
help="BirdNET result type (default: csv)")
|
|
82
|
+
parser.add_argument("--min_conf_flag", type=float, default=0.5,
|
|
83
|
+
help="Minimum confidence for raw output (default: 0.5)")
|
|
84
|
+
|
|
85
|
+
# Also support positional fallback for simple invocation
|
|
86
|
+
if len(sys.argv) >= 3 and not sys.argv[2].startswith("--"):
|
|
87
|
+
# Called as: script.py <audio_dir> <output_dir>
|
|
88
|
+
ns = parser.parse_args(sys.argv[1:])
|
|
89
|
+
else:
|
|
90
|
+
ns = parser.parse_args()
|
|
91
|
+
return ns
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def discover_audio_files(audio_dir: Path) -> list[Path]:
|
|
95
|
+
"""Return all .wav and .flac files in audio_dir (recursive)."""
|
|
96
|
+
files = []
|
|
97
|
+
for ext in ("*.wav", "*.WAV", "*.flac", "*.FLAC"):
|
|
98
|
+
files.extend(audio_dir.rglob(ext))
|
|
99
|
+
return sorted(files)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def parse_timestamp_from_filename(fname: str):
|
|
103
|
+
"""Extract datetime from common recorder filename patterns.
|
|
104
|
+
Returns datetime or None.
|
|
105
|
+
"""
|
|
106
|
+
patterns = [
|
|
107
|
+
r"(\d{8})[_$T](\d{6})", # AUDIOMOTH_20240601_050000
|
|
108
|
+
r"(\d{4}-\d{2}-\d{2})T(\d{2}-\d{2}-\d{2})", # ISO variant
|
|
109
|
+
]
|
|
110
|
+
for pat in patterns:
|
|
111
|
+
m = re.search(pat, fname)
|
|
112
|
+
if m:
|
|
113
|
+
date_s, time_s = m.group(1), m.group(2)
|
|
114
|
+
date_s = date_s.replace("-", "")
|
|
115
|
+
time_s = time_s.replace("-", "")
|
|
116
|
+
try:
|
|
117
|
+
return datetime.strptime(date_s + time_s, "%Y%m%d%H%M%S")
|
|
118
|
+
except ValueError:
|
|
119
|
+
continue
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def run_birdnet(audio_file: Path, output_dir: Path, args) -> Path | None:
|
|
124
|
+
"""Run BirdNET-Analyzer on a single file. Returns path to result file."""
|
|
125
|
+
result_dir = output_dir / "birdnet_raw"
|
|
126
|
+
result_dir.mkdir(parents=True, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
result_file = result_dir / (audio_file.stem + f".BirdNET.results.{args.rtype}")
|
|
129
|
+
|
|
130
|
+
cmd = [
|
|
131
|
+
BIRDNET_CMD,
|
|
132
|
+
"--i", str(audio_file),
|
|
133
|
+
"--o", str(result_dir),
|
|
134
|
+
"--min_conf", str(args.min_conf_flag),
|
|
135
|
+
"--overlap", str(args.overlap),
|
|
136
|
+
"--rtype", args.rtype,
|
|
137
|
+
]
|
|
138
|
+
if args.lat is not None and args.lon is not None:
|
|
139
|
+
cmd += ["--lat", str(args.lat), "--lon", str(args.lon)]
|
|
140
|
+
if args.date is not None:
|
|
141
|
+
cmd += ["--week", _date_to_week(args.date)]
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
145
|
+
if proc.returncode != 0:
|
|
146
|
+
logger.warning("BirdNET falhou para %s: %s", audio_file.name, proc.stderr[:200])
|
|
147
|
+
return None
|
|
148
|
+
return result_file if result_file.exists() else None
|
|
149
|
+
except FileNotFoundError:
|
|
150
|
+
logger.error(
|
|
151
|
+
"BirdNET nao encontrado. Configure BIRDNET_PATH ou instale birdnet_analyzer\n Causa provavel: BirdNET-Analyzer nao instalado ou nao esta no PATH\n Skill anterior: [nenhuma — dependencia externa]"
|
|
152
|
+
)
|
|
153
|
+
sys.exit(1)
|
|
154
|
+
except subprocess.TimeoutExpired:
|
|
155
|
+
logger.warning("Timeout ao processar %s", audio_file.name)
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _date_to_week(date_str: str) -> str:
|
|
160
|
+
"""Convert YYYY-MM-DD to BirdNET week number (1-48)."""
|
|
161
|
+
try:
|
|
162
|
+
d = datetime.strptime(date_str, "%Y-%m-%d")
|
|
163
|
+
# BirdNET uses 48 weeks (each ~7.6 days)
|
|
164
|
+
week = min(48, max(1, int((d.timetuple().tm_yday - 1) / 7.625) + 1))
|
|
165
|
+
return str(week)
|
|
166
|
+
except ValueError:
|
|
167
|
+
return "1"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def parse_birdnet_csv(result_file: Path) -> list[dict]:
|
|
171
|
+
"""Parse BirdNET CSV output into list of detection dicts."""
|
|
172
|
+
detections = []
|
|
173
|
+
try:
|
|
174
|
+
with open(result_file, newline="", encoding="utf-8") as f:
|
|
175
|
+
reader = csv.DictReader(f, delimiter="\t")
|
|
176
|
+
if reader.fieldnames is None:
|
|
177
|
+
return detections
|
|
178
|
+
for row in reader:
|
|
179
|
+
# Normalize column names (BirdNET versions differ)
|
|
180
|
+
det = {
|
|
181
|
+
"start_s": float(row.get("Start (s)", row.get("start", 0))),
|
|
182
|
+
"end_s": float(row.get("End (s)", row.get("end", 0))),
|
|
183
|
+
"species_code": row.get("Label", row.get("label", "")).strip(),
|
|
184
|
+
"common_name": row.get("Common name", row.get("common_name", "")).strip(),
|
|
185
|
+
"confidence": float(row.get("Confidence", row.get("confidence", 0))),
|
|
186
|
+
}
|
|
187
|
+
detections.append(det)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.warning("Nao foi possivel analisar %s: %s", result_file.name, e)
|
|
190
|
+
return detections
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def main():
|
|
194
|
+
args = parse_args()
|
|
195
|
+
audio_dir = Path(args.audio_dir)
|
|
196
|
+
output_dir = Path(args.output_dir)
|
|
197
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
|
|
199
|
+
# ── Input precondition checks ────────────────────────────────────────────
|
|
200
|
+
if not audio_dir.is_dir():
|
|
201
|
+
logger.error(
|
|
202
|
+
"Input nao encontrado: %s\n Causa provavel: caminho incorreto ou diretorio nao montado\n Skill anterior: [nenhuma — etapa inicial]",
|
|
203
|
+
audio_dir,
|
|
204
|
+
)
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
|
|
207
|
+
log_decision("confidence", args.confidence,
|
|
208
|
+
"limiar minimo de confianca para filtrar deteccoes; >= 0.7 recomendado")
|
|
209
|
+
log_decision("lat/lon", f"{args.lat}/{args.lon}",
|
|
210
|
+
"coordenadas geograficas melhoram o filtro de especies do BirdNET")
|
|
211
|
+
log_decision("overlap", args.overlap,
|
|
212
|
+
"sobreposicao de segmentos em segundos; 0 = sem sobreposicao")
|
|
213
|
+
|
|
214
|
+
log_step(1, "Descobrindo arquivos de audio")
|
|
215
|
+
audio_files = discover_audio_files(audio_dir)
|
|
216
|
+
if not audio_files:
|
|
217
|
+
logger.error(
|
|
218
|
+
"Nenhum arquivo de audio encontrado em %s\n Causa provavel: diretorio vazio ou extensoes nao suportadas\n Skill anterior: [nenhuma]",
|
|
219
|
+
audio_dir,
|
|
220
|
+
)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
logger.info("Encontrados %d arquivos de audio", len(audio_files))
|
|
223
|
+
logger.info("Limiar de confianca: %s", args.confidence)
|
|
224
|
+
|
|
225
|
+
log_step(2, "Executando BirdNET em cada arquivo de audio")
|
|
226
|
+
all_detections = []
|
|
227
|
+
|
|
228
|
+
for i, fpath in enumerate(audio_files, 1):
|
|
229
|
+
logger.info(" [%d/%d] %s", i, len(audio_files), fpath.name)
|
|
230
|
+
file_ts = parse_timestamp_from_filename(fpath.name)
|
|
231
|
+
if file_ts is None:
|
|
232
|
+
logger.warning("Timestamp nao extraido do nome do arquivo: %s", fpath.name)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
result_file = run_birdnet(fpath, output_dir, args)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
logger.error("Unexpected error in run_birdnet for %s: %s", fpath.name, e)
|
|
238
|
+
raise
|
|
239
|
+
if result_file is None:
|
|
240
|
+
continue
|
|
241
|
+
|
|
242
|
+
raw_dets = parse_birdnet_csv(result_file)
|
|
243
|
+
for det in raw_dets:
|
|
244
|
+
det["file"] = fpath.name
|
|
245
|
+
if file_ts is not None:
|
|
246
|
+
abs_time = file_ts + timedelta(seconds=det["start_s"])
|
|
247
|
+
det["datetime"] = abs_time.strftime("%Y-%m-%d %H:%M:%S")
|
|
248
|
+
det["hour"] = abs_time.hour
|
|
249
|
+
det["date"] = abs_time.strftime("%Y-%m-%d")
|
|
250
|
+
else:
|
|
251
|
+
det["datetime"] = ""
|
|
252
|
+
det["hour"] = -1
|
|
253
|
+
det["date"] = ""
|
|
254
|
+
all_detections.append(det)
|
|
255
|
+
|
|
256
|
+
if not all_detections:
|
|
257
|
+
logger.warning(
|
|
258
|
+
"Nenhuma deteccao produzida. Verifique a instalacao do BirdNET e os arquivos de audio."
|
|
259
|
+
)
|
|
260
|
+
sys.exit(0)
|
|
261
|
+
|
|
262
|
+
logger.info("Total de deteccoes brutas: %d", len(all_detections))
|
|
263
|
+
|
|
264
|
+
log_step(3, "Escrevendo deteccoes brutas e filtradas")
|
|
265
|
+
# ── Write raw detections ────────────────────────────────────────────────
|
|
266
|
+
fieldnames = ["file", "datetime", "date", "hour",
|
|
267
|
+
"start_s", "end_s", "species_code", "common_name", "confidence"]
|
|
268
|
+
raw_path = output_dir / "detections_raw.csv"
|
|
269
|
+
try:
|
|
270
|
+
with open(raw_path, "w", newline="", encoding="utf-8") as f:
|
|
271
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
272
|
+
writer.writeheader()
|
|
273
|
+
writer.writerows(all_detections)
|
|
274
|
+
logger.info("Deteccoes brutas: %d -> %s", len(all_detections), raw_path)
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error("Unexpected error writing raw detections: %s", e)
|
|
277
|
+
raise
|
|
278
|
+
|
|
279
|
+
# ── Filter by confidence ────────────────────────────────────────────────
|
|
280
|
+
filtered = [d for d in all_detections if d["confidence"] >= args.confidence]
|
|
281
|
+
filt_path = output_dir / "detections_filtered.csv"
|
|
282
|
+
with open(filt_path, "w", newline="", encoding="utf-8") as f:
|
|
283
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
284
|
+
writer.writeheader()
|
|
285
|
+
writer.writerows(filtered)
|
|
286
|
+
logger.info("Deteccoes filtradas (>=%s): %d -> %s", args.confidence, len(filtered), filt_path)
|
|
287
|
+
|
|
288
|
+
if len(filtered) == 0:
|
|
289
|
+
logger.warning(
|
|
290
|
+
"Nenhuma deteccao apos filtro de confianca %.2f; considere reduzir o limiar",
|
|
291
|
+
args.confidence,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
log_step(4, "Construindo lista de especies detectadas")
|
|
295
|
+
# ── Species list ────────────────────────────────────────────────────────
|
|
296
|
+
species_stats = defaultdict(lambda: {"n": 0, "max_conf": 0.0, "dates": set()})
|
|
297
|
+
for d in filtered:
|
|
298
|
+
sp = d["common_name"] or d["species_code"]
|
|
299
|
+
species_stats[sp]["n"] += 1
|
|
300
|
+
species_stats[sp]["max_conf"] = max(species_stats[sp]["max_conf"], d["confidence"])
|
|
301
|
+
if d["date"]:
|
|
302
|
+
species_stats[sp]["dates"].add(d["date"])
|
|
303
|
+
|
|
304
|
+
sp_path = output_dir / "species_list.csv"
|
|
305
|
+
with open(sp_path, "w", newline="", encoding="utf-8") as f:
|
|
306
|
+
writer = csv.writer(f)
|
|
307
|
+
writer.writerow(["species", "n_detections", "max_confidence",
|
|
308
|
+
"n_dates", "confidence_category"])
|
|
309
|
+
for sp, stats in sorted(species_stats.items(), key=lambda x: -x[1]["n"]):
|
|
310
|
+
conf_cat = ("high" if stats["max_conf"] >= 0.9 else
|
|
311
|
+
"medium" if stats["max_conf"] >= 0.7 else "low")
|
|
312
|
+
writer.writerow([sp, stats["n"], round(stats["max_conf"], 3),
|
|
313
|
+
len(stats["dates"]), conf_cat])
|
|
314
|
+
logger.info("Especies detectadas: %d -> %s", len(species_stats), sp_path)
|
|
315
|
+
|
|
316
|
+
log_step(5, "Gerando resumo de deteccoes por hora")
|
|
317
|
+
# ── Detection summary by hour ────────────────────────────────────────────
|
|
318
|
+
hour_counts = defaultdict(lambda: defaultdict(int))
|
|
319
|
+
for d in filtered:
|
|
320
|
+
if d["hour"] >= 0:
|
|
321
|
+
sp = d["common_name"] or d["species_code"]
|
|
322
|
+
hour_counts[sp][d["hour"]] += 1
|
|
323
|
+
|
|
324
|
+
if hour_counts:
|
|
325
|
+
all_hours = list(range(24))
|
|
326
|
+
all_sps = sorted(hour_counts.keys())
|
|
327
|
+
sum_path = output_dir / "detection_summary.csv"
|
|
328
|
+
with open(sum_path, "w", newline="", encoding="utf-8") as f:
|
|
329
|
+
writer = csv.writer(f)
|
|
330
|
+
writer.writerow(["species"] + [str(h) for h in all_hours])
|
|
331
|
+
for sp in all_sps:
|
|
332
|
+
writer.writerow([sp] + [hour_counts[sp].get(h, 0) for h in all_hours])
|
|
333
|
+
logger.info("Resumo de deteccoes por hora -> %s", sum_path)
|
|
334
|
+
else:
|
|
335
|
+
logger.warning(
|
|
336
|
+
"Sem timestamps horarios disponíveis; detection_summary.csv nao gerado"
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
log_step(6, "Calculando acumulacao de especies")
|
|
340
|
+
# ── Species accumulation (text) ─────────────────────────────────────────
|
|
341
|
+
seen = set()
|
|
342
|
+
accumulation = []
|
|
343
|
+
for i, d in enumerate(filtered, 1):
|
|
344
|
+
sp = d["common_name"] or d["species_code"]
|
|
345
|
+
seen.add(sp)
|
|
346
|
+
if i % max(1, len(filtered) // 50) == 0 or i == len(filtered):
|
|
347
|
+
accumulation.append({"n_detections": i, "cumulative_species": len(seen)})
|
|
348
|
+
|
|
349
|
+
accum_path = output_dir / "species_accumulation.csv"
|
|
350
|
+
with open(accum_path, "w", newline="", encoding="utf-8") as f:
|
|
351
|
+
writer = csv.DictWriter(f, fieldnames=["n_detections", "cumulative_species"])
|
|
352
|
+
writer.writeheader()
|
|
353
|
+
writer.writerows(accumulation)
|
|
354
|
+
logger.info("Acumulacao de especies -> %s", accum_path)
|
|
355
|
+
|
|
356
|
+
logger.info("Deteccao em lote concluida")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
if __name__ == "__main__":
|
|
360
|
+
main()
|