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,235 @@
|
|
|
1
|
+
# ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
# Usage: Rscript compute_acoustic_indices.R <audio_dir> <output_dir> [time_resolution_min] [freq_min] [freq_max]
|
|
5
|
+
#
|
|
6
|
+
# Computes acoustic indices (ACI, BI, NDSI, H, ADI, AEI) for all .wav/.flac
|
|
7
|
+
# files in a directory, aggregates by time resolution, and produces summary
|
|
8
|
+
# outputs and a soundscape heatmap.
|
|
9
|
+
#
|
|
10
|
+
# Outputs:
|
|
11
|
+
# acoustic_indices_timeseries.csv — per-file index values with timestamps
|
|
12
|
+
# indices_summary.csv — mean ± SD per index per hour-of-day
|
|
13
|
+
# soundscape_plot.png — heatmap (hour × day × index)
|
|
14
|
+
|
|
15
|
+
# ── Inline logger ─────────────────────────────────────────────────────────────
|
|
16
|
+
SKILL_NAME <- "acoustic-monitoring"
|
|
17
|
+
.log_ts <- function() format(Sys.time(), "[%Y-%m-%d %H:%M:%S]")
|
|
18
|
+
log_info <- function(...) message(.log_ts(), " [INFO] ", sprintf(...))
|
|
19
|
+
log_warn <- function(...) message(.log_ts(), " [WARN] ", sprintf(...))
|
|
20
|
+
log_error<- function(...) message(.log_ts(), " [ERROR] ", sprintf(...))
|
|
21
|
+
log_step <- function(n, d) log_info("-- STEP %d: %s", n, d)
|
|
22
|
+
log_decision <- function(v, val, why) log_info("DECISION | %s = %s | %s", v, val, why)
|
|
23
|
+
dir.create("logs", recursive=TRUE, showWarnings=FALSE)
|
|
24
|
+
|
|
25
|
+
suppressPackageStartupMessages(library(soundecology))
|
|
26
|
+
suppressPackageStartupMessages(library(tuneR))
|
|
27
|
+
suppressPackageStartupMessages(library(dplyr))
|
|
28
|
+
suppressPackageStartupMessages(library(tidyr))
|
|
29
|
+
suppressPackageStartupMessages(library(lubridate))
|
|
30
|
+
suppressPackageStartupMessages(library(ggplot2))
|
|
31
|
+
|
|
32
|
+
args <- commandArgs(trailingOnly = TRUE)
|
|
33
|
+
if (length(args) < 2) {
|
|
34
|
+
log_error("Argumentos insuficientes. Uso: Rscript compute_acoustic_indices.R <audio_dir> <output_dir> [time_resolution_min] [freq_min_hz] [freq_max_hz]")
|
|
35
|
+
cat("Usage: Rscript compute_acoustic_indices.R <audio_dir> <output_dir>",
|
|
36
|
+
"[time_resolution_min] [freq_min_hz] [freq_max_hz]\n")
|
|
37
|
+
cat("Defaults: time_resolution_min=1 freq_min_hz=0 freq_max_hz=22050\n")
|
|
38
|
+
quit(status = 1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
audio_dir <- args[1]
|
|
42
|
+
output_dir <- args[2]
|
|
43
|
+
time_res_min <- if (length(args) >= 3) as.integer(args[3]) else 1L
|
|
44
|
+
freq_min_hz <- if (length(args) >= 4) as.numeric(args[4]) else 0
|
|
45
|
+
freq_max_hz <- if (length(args) >= 5) as.numeric(args[5]) else 22050
|
|
46
|
+
|
|
47
|
+
# ── Input precondition checks ────────────────────────────────────────────────
|
|
48
|
+
if (!dir.exists(audio_dir)) {
|
|
49
|
+
log_error("Input nao encontrado: %s\nCausa provavel: caminho incorreto ou diretorio nao montado\nVerifique: se o diretorio de audio existe\nSkill anterior: [nenhuma — etapa inicial]", audio_dir)
|
|
50
|
+
stop("Missing audio_dir: ", audio_dir)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log_decision("time_res_min", time_res_min,
|
|
54
|
+
"resolucao temporal de agregacao em minutos; 1 min = resolucao completa por arquivo")
|
|
55
|
+
log_decision("freq_min_hz", freq_min_hz,
|
|
56
|
+
"frequencia minima de analise em Hz; 0 = sem filtro inferior")
|
|
57
|
+
log_decision("freq_max_hz", freq_max_hz,
|
|
58
|
+
"frequencia maxima de analise em Hz; limitado pela taxa de amostragem do arquivo")
|
|
59
|
+
|
|
60
|
+
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
|
|
61
|
+
|
|
62
|
+
log_step(1, "Descobrindo arquivos de audio no diretorio")
|
|
63
|
+
# ── Discover audio files ────────────────────────────────────────────────────
|
|
64
|
+
wav_files <- list.files(audio_dir, pattern = "\\.wav$", full.names = TRUE,
|
|
65
|
+
recursive = TRUE, ignore.case = TRUE)
|
|
66
|
+
flac_files <- list.files(audio_dir, pattern = "\\.flac$", full.names = TRUE,
|
|
67
|
+
recursive = TRUE, ignore.case = TRUE)
|
|
68
|
+
audio_files <- c(wav_files, flac_files)
|
|
69
|
+
|
|
70
|
+
if (length(audio_files) == 0) {
|
|
71
|
+
log_error("Nenhum arquivo .wav ou .flac encontrado em: %s\nCausa provavel: diretorio vazio ou extensoes em maiusculas nao reconhecidas\nVerifique: conteudo do diretorio de audio\nSkill anterior: [nenhuma]", audio_dir)
|
|
72
|
+
stop("No .wav or .flac files found in: ", audio_dir)
|
|
73
|
+
}
|
|
74
|
+
log_info("Encontrados %d arquivos de audio", length(audio_files))
|
|
75
|
+
|
|
76
|
+
# ── Helper: extract timestamp from filename ─────────────────────────────────
|
|
77
|
+
# Supports common recorder filename patterns:
|
|
78
|
+
# AUDIOMOTH_20240601_050000.wav
|
|
79
|
+
# SMM07207_0+1_20240601$050000.wav (Wildlife Acoustics)
|
|
80
|
+
# generic: any 8-digit date + 6-digit time
|
|
81
|
+
parse_timestamp <- function(fname) {
|
|
82
|
+
base <- tools::file_path_sans_ext(basename(fname))
|
|
83
|
+
m <- regmatches(base,
|
|
84
|
+
regexpr("(\\d{8})[_$T](\\d{6})", base, perl = TRUE))
|
|
85
|
+
if (length(m) == 0 || nchar(m) == 0) return(NA_POSIXct_)
|
|
86
|
+
parts <- regmatches(m, regexpr("(\\d{8})[_$T](\\d{6})", m, perl = TRUE))
|
|
87
|
+
dt_str <- sub("([_$T])", " ", parts)
|
|
88
|
+
tryCatch(as.POSIXct(dt_str, format = "%Y%m%d %H%M%S", tz = "UTC"),
|
|
89
|
+
error = function(e) NA_POSIXct_)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
log_step(2, "Computando indices acusticos por arquivo")
|
|
93
|
+
# ── Compute indices per file ────────────────────────────────────────────────
|
|
94
|
+
compute_file_indices <- function(fpath) {
|
|
95
|
+
tryCatch({
|
|
96
|
+
if (grepl("\\.flac$", fpath, ignore.case = TRUE)) {
|
|
97
|
+
# Convert FLAC to temp WAV for tuneR compatibility
|
|
98
|
+
tmp <- tempfile(fileext = ".wav")
|
|
99
|
+
system2("ffmpeg", args = c("-y", "-i", shQuote(fpath),
|
|
100
|
+
shQuote(tmp)), stdout = FALSE, stderr = FALSE)
|
|
101
|
+
wave <- readWave(tmp)
|
|
102
|
+
file.remove(tmp)
|
|
103
|
+
} else {
|
|
104
|
+
wave <- readWave(fpath)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
sr <- wave@samp.rate
|
|
108
|
+
freq_max_use <- min(freq_max_hz, sr / 2)
|
|
109
|
+
|
|
110
|
+
# ACI — Acoustic Complexity Index
|
|
111
|
+
aci_res <- acoustic_complexity(wave, min_freq = freq_min_hz,
|
|
112
|
+
max_freq = freq_max_use)
|
|
113
|
+
aci_val <- aci_res$AciTotAll_left
|
|
114
|
+
|
|
115
|
+
# BI — Bioacoustic Index
|
|
116
|
+
bi_res <- bioacoustic_index(wave, min_freq = max(freq_min_hz, 2000),
|
|
117
|
+
max_freq = min(freq_max_use, 8000))
|
|
118
|
+
bi_val <- bi_res$left_area
|
|
119
|
+
|
|
120
|
+
# NDSI — Normalized Difference Soundscape Index
|
|
121
|
+
ndsi_res <- ndsi(wave, fft_w = 1024,
|
|
122
|
+
anthro_min = max(freq_min_hz, 200),
|
|
123
|
+
anthro_max = 2000,
|
|
124
|
+
bio_min = 2000,
|
|
125
|
+
bio_max = min(freq_max_use, 8000))
|
|
126
|
+
ndsi_val <- ndsi_res$ndsi_left
|
|
127
|
+
|
|
128
|
+
# H — Spectral and temporal entropy
|
|
129
|
+
h_res <- acoustic_entropy(wave)
|
|
130
|
+
h_val <- h_res$H
|
|
131
|
+
|
|
132
|
+
# ADI — Acoustic Diversity Index
|
|
133
|
+
adi_res <- acoustic_diversity(wave, max_freq = freq_max_use, db_threshold = -50)
|
|
134
|
+
adi_val <- adi_res$adi_left
|
|
135
|
+
|
|
136
|
+
# AEI — Acoustic Evenness Index
|
|
137
|
+
aei_res <- acoustic_evenness(wave, max_freq = freq_max_use, db_threshold = -50)
|
|
138
|
+
aei_val <- aei_res$aei_left
|
|
139
|
+
|
|
140
|
+
data.frame(
|
|
141
|
+
file = basename(fpath),
|
|
142
|
+
datetime = parse_timestamp(fpath),
|
|
143
|
+
ACI = aci_val,
|
|
144
|
+
BI = bi_val,
|
|
145
|
+
NDSI = ndsi_val,
|
|
146
|
+
H = h_val,
|
|
147
|
+
ADI = adi_val,
|
|
148
|
+
AEI = aei_val,
|
|
149
|
+
stringsAsFactors = FALSE
|
|
150
|
+
)
|
|
151
|
+
}, error = function(e) {
|
|
152
|
+
log_warn("Pulando %s: %s", basename(fpath), conditionMessage(e))
|
|
153
|
+
NULL
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
log_info("Computando indices — pode levar varios minutos para grandes conjuntos de dados...")
|
|
158
|
+
results_list <- lapply(audio_files, compute_file_indices)
|
|
159
|
+
results_list <- Filter(Negate(is.null), results_list)
|
|
160
|
+
|
|
161
|
+
if (length(results_list) == 0) {
|
|
162
|
+
log_error("Nenhum arquivo processado com sucesso\nCausa provavel: formato de audio incompativel ou intervalo de frequencia invalido\nVerifique: formato WAV/FLAC e configuracoes de frequencia\nSkill anterior: [nenhuma]")
|
|
163
|
+
stop("No files could be processed. Check audio format and frequency range.")
|
|
164
|
+
}
|
|
165
|
+
log_info("%d de %d arquivos processados com sucesso", length(results_list), length(audio_files))
|
|
166
|
+
|
|
167
|
+
log_step(3, "Agregando indices por resolucao temporal")
|
|
168
|
+
indices_df <- dplyr::bind_rows(results_list)
|
|
169
|
+
|
|
170
|
+
# ── Aggregate by time resolution ────────────────────────────────────────────
|
|
171
|
+
if (!is.na(indices_df$datetime[1])) {
|
|
172
|
+
indices_df <- indices_df %>%
|
|
173
|
+
mutate(
|
|
174
|
+
time_block = floor_date(datetime, unit = paste(time_res_min, "mins")),
|
|
175
|
+
hour_of_day = hour(datetime),
|
|
176
|
+
date = as.Date(datetime)
|
|
177
|
+
)
|
|
178
|
+
} else {
|
|
179
|
+
# No timestamps extracted; use row order as proxy
|
|
180
|
+
indices_df <- indices_df %>%
|
|
181
|
+
mutate(time_block = seq_len(nrow(.)),
|
|
182
|
+
hour_of_day = NA_integer_,
|
|
183
|
+
date = NA)
|
|
184
|
+
log_warn("Nenhum timestamp extraido dos nomes de arquivo. Heatmap nao sera gerado.")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
log_step(4, "Escrevendo CSV de serie temporal de indices")
|
|
188
|
+
# ── Write timeseries CSV ────────────────────────────────────────────────────
|
|
189
|
+
ts_path <- file.path(output_dir, "acoustic_indices_timeseries.csv")
|
|
190
|
+
write.csv(indices_df, ts_path, row.names = FALSE)
|
|
191
|
+
log_info("Serie temporal escrita: %s (%d linhas)", ts_path, nrow(indices_df))
|
|
192
|
+
|
|
193
|
+
log_step(5, "Calculando e escrevendo resumo por hora do dia")
|
|
194
|
+
# ── Write summary CSV ───────────────────────────────────────────────────────
|
|
195
|
+
summary_df <- indices_df %>%
|
|
196
|
+
group_by(hour_of_day) %>%
|
|
197
|
+
summarise(
|
|
198
|
+
across(c(ACI, BI, NDSI, H, ADI, AEI),
|
|
199
|
+
list(mean = ~mean(.x, na.rm = TRUE),
|
|
200
|
+
sd = ~sd(.x, na.rm = TRUE))),
|
|
201
|
+
n_recordings = n(),
|
|
202
|
+
.groups = "drop"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
sum_path <- file.path(output_dir, "indices_summary.csv")
|
|
206
|
+
write.csv(summary_df, sum_path, row.names = FALSE)
|
|
207
|
+
log_info("Resumo escrito: %s", sum_path)
|
|
208
|
+
|
|
209
|
+
log_step(6, "Gerando heatmap de paisagem sonora")
|
|
210
|
+
# ── Soundscape heatmap ──────────────────────────────────────────────────────
|
|
211
|
+
if (!all(is.na(indices_df$datetime))) {
|
|
212
|
+
plot_df <- indices_df %>%
|
|
213
|
+
select(date, hour_of_day, ACI, NDSI, H) %>%
|
|
214
|
+
pivot_longer(cols = c(ACI, NDSI, H), names_to = "index", values_to = "value") %>%
|
|
215
|
+
group_by(date, hour_of_day, index) %>%
|
|
216
|
+
summarise(value = mean(value, na.rm = TRUE), .groups = "drop")
|
|
217
|
+
|
|
218
|
+
p <- ggplot(plot_df, aes(x = hour_of_day, y = as.factor(date), fill = value)) +
|
|
219
|
+
geom_tile() +
|
|
220
|
+
scale_fill_viridis_c(option = "magma", na.value = "grey80") +
|
|
221
|
+
facet_wrap(~index, ncol = 1, scales = "free_x") +
|
|
222
|
+
labs(x = "Hour of day", y = "Date", fill = "Index value",
|
|
223
|
+
title = "Soundscape index heatmap (ACI, NDSI, H)") +
|
|
224
|
+
theme_minimal(base_size = 10) +
|
|
225
|
+
theme(axis.text.y = element_text(size = 7))
|
|
226
|
+
|
|
227
|
+
plot_path <- file.path(output_dir, "soundscape_plot.png")
|
|
228
|
+
ggsave(plot_path, p, width = 10, height = max(4, nrow(unique(plot_df[, "date"])) * 0.4 + 2),
|
|
229
|
+
dpi = 150)
|
|
230
|
+
log_info("Heatmap salvo: %s", plot_path)
|
|
231
|
+
} else {
|
|
232
|
+
log_warn("Pulando heatmap: nenhum timestamp disponivel nos arquivos de audio")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
log_info("Computacao de indices acusticos concluida")
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# ecological-agent-skills / Copyright (C) 2026 Francisco Diego Barros Barata
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Compute acoustic indices for passive acoustic monitoring using librosa and soundfile.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
python compute_acoustic_indices.py <audio_dir> <output_dir>
|
|
9
|
+
[--resolution_min 1] [--freq_min 0] [--freq_max 22050]
|
|
10
|
+
|
|
11
|
+
Implements:
|
|
12
|
+
ACI — Acoustic Complexity Index (Pieretti et al. 2011)
|
|
13
|
+
BI — Bioacoustic Index (Boelman et al. 2007), approximated
|
|
14
|
+
NDSI — Normalized Difference Soundscape Index (Kasten et al. 2012)
|
|
15
|
+
Ht — Temporal entropy (Sueur et al. 2008)
|
|
16
|
+
Hf — Spectral entropy (Sueur et al. 2008)
|
|
17
|
+
H — Total entropy (Ht × Hf)
|
|
18
|
+
|
|
19
|
+
Outputs:
|
|
20
|
+
acoustic_indices_timeseries.csv — per-file indices with timestamps
|
|
21
|
+
indices_summary.csv — mean ± SD by hour of day
|
|
22
|
+
soundscape_plot.png — heatmap (date × hour, coloured by ACI)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import sys
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
SKILL_NAME = "acoustic-monitoring"
|
|
31
|
+
_LOG_DIR = Path("logs")
|
|
32
|
+
_LOG_DIR.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
_log_file = _LOG_DIR / f"skill_{SKILL_NAME}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log"
|
|
34
|
+
logging.basicConfig(
|
|
35
|
+
level=logging.INFO,
|
|
36
|
+
format="[%(asctime)s] [%(levelname)s] [" + SKILL_NAME + "] %(message)s",
|
|
37
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
38
|
+
handlers=[
|
|
39
|
+
logging.StreamHandler(sys.stdout),
|
|
40
|
+
logging.FileHandler(_log_file, encoding="utf-8"),
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
logger = logging.getLogger(SKILL_NAME)
|
|
44
|
+
|
|
45
|
+
def log_step(n: int, desc: str) -> None:
|
|
46
|
+
logger.info("-- STEP %d: %s", n, desc)
|
|
47
|
+
|
|
48
|
+
def log_decision(var: str, val, why: str) -> None:
|
|
49
|
+
logger.info("DECISION | %s = %s | %s", var, val, why)
|
|
50
|
+
|
|
51
|
+
import os
|
|
52
|
+
import re
|
|
53
|
+
import csv
|
|
54
|
+
import math
|
|
55
|
+
import argparse
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
import numpy as np
|
|
59
|
+
import librosa
|
|
60
|
+
import soundfile as sf
|
|
61
|
+
except ImportError as e:
|
|
62
|
+
logger.error("Missing dependency: %s. Install with: pip install librosa soundfile numpy", e)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def parse_args():
|
|
67
|
+
parser = argparse.ArgumentParser(
|
|
68
|
+
description="Compute acoustic indices for PAM recordings"
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument("audio_dir", help="Directory containing .wav/.flac files")
|
|
71
|
+
parser.add_argument("output_dir", help="Directory for output files")
|
|
72
|
+
parser.add_argument("--resolution_min", type=int, default=1,
|
|
73
|
+
help="Time aggregation resolution in minutes (default: 1)")
|
|
74
|
+
parser.add_argument("--freq_min", type=float, default=0,
|
|
75
|
+
help="Minimum frequency in Hz (default: 0)")
|
|
76
|
+
parser.add_argument("--freq_max", type=float, default=22050,
|
|
77
|
+
help="Maximum frequency in Hz (default: 22050)")
|
|
78
|
+
# Fallback positional for simple invocation
|
|
79
|
+
if len(sys.argv) >= 3 and not sys.argv[1].startswith("--"):
|
|
80
|
+
return parser.parse_args()
|
|
81
|
+
return parser.parse_args()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_timestamp(fname: str):
|
|
85
|
+
"""Extract datetime from common logger filename patterns."""
|
|
86
|
+
patterns = [r"(\d{8})[_$T](\d{6})"]
|
|
87
|
+
for pat in patterns:
|
|
88
|
+
m = re.search(pat, fname)
|
|
89
|
+
if m:
|
|
90
|
+
try:
|
|
91
|
+
return datetime.strptime(m.group(1) + m.group(2), "%Y%m%d%H%M%S")
|
|
92
|
+
except ValueError:
|
|
93
|
+
pass
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Index computation functions ─────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def compute_aci(y: np.ndarray, sr: int, n_fft: int = 1024, hop: int = 512,
|
|
100
|
+
freq_min: float = 0, freq_max: float = 22050) -> float:
|
|
101
|
+
"""Acoustic Complexity Index (Pieretti et al. 2011).
|
|
102
|
+
|
|
103
|
+
ACI = sum(|D_k|) / sum(I_k) per frequency bin, summed across bins.
|
|
104
|
+
D_k = absolute difference between adjacent time frames in bin k.
|
|
105
|
+
I_k = sum of intensities in bin k.
|
|
106
|
+
"""
|
|
107
|
+
S = np.abs(librosa.stft(y, n_fft=n_fft, hop_length=hop))
|
|
108
|
+
freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft)
|
|
109
|
+
mask = (freqs >= freq_min) & (freqs <= freq_max)
|
|
110
|
+
S = S[mask, :]
|
|
111
|
+
if S.shape[1] < 2:
|
|
112
|
+
return float("nan")
|
|
113
|
+
diffs = np.abs(np.diff(S, axis=1))
|
|
114
|
+
totals = np.sum(S[:, :-1], axis=1)
|
|
115
|
+
totals[totals == 0] = np.finfo(float).eps
|
|
116
|
+
aci = float(np.sum(np.sum(diffs, axis=1) / totals))
|
|
117
|
+
return aci
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def compute_bi(y: np.ndarray, sr: int, n_fft: int = 1024, hop: int = 512,
|
|
121
|
+
bio_min: float = 2000, bio_max: float = 8000) -> float:
|
|
122
|
+
"""Bioacoustic Index — area under mean spectrum in biophony band (2–8 kHz).
|
|
123
|
+
Approximation of Boelman et al. (2007).
|
|
124
|
+
"""
|
|
125
|
+
S = np.abs(librosa.stft(y, n_fft=n_fft, hop_length=hop))
|
|
126
|
+
freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft)
|
|
127
|
+
mask = (freqs >= bio_min) & (freqs <= min(bio_max, sr / 2))
|
|
128
|
+
S_bio = S[mask, :]
|
|
129
|
+
if S_bio.size == 0:
|
|
130
|
+
return float("nan")
|
|
131
|
+
mean_spec = np.mean(S_bio, axis=1)
|
|
132
|
+
mean_db = 20 * np.log10(mean_spec + np.finfo(float).eps)
|
|
133
|
+
mean_db = np.clip(mean_db - mean_db.min(), 0, None)
|
|
134
|
+
bi = float(np.trapz(mean_db))
|
|
135
|
+
return bi
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def compute_ndsi(y: np.ndarray, sr: int, n_fft: int = 1024, hop: int = 512,
|
|
139
|
+
anthro_min: float = 200, anthro_max: float = 2000,
|
|
140
|
+
bio_min: float = 2000, bio_max: float = 8000) -> float:
|
|
141
|
+
"""Normalized Difference Soundscape Index (Kasten et al. 2012).
|
|
142
|
+
NDSI = (biophony - anthrophony) / (biophony + anthrophony)
|
|
143
|
+
"""
|
|
144
|
+
S = np.abs(librosa.stft(y, n_fft=n_fft, hop_length=hop)) ** 2
|
|
145
|
+
freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft)
|
|
146
|
+
anthro_mask = (freqs >= anthro_min) & (freqs < anthro_max)
|
|
147
|
+
bio_mask = (freqs >= bio_min) & (freqs <= min(bio_max, sr / 2))
|
|
148
|
+
anthro = float(np.sum(S[anthro_mask, :]))
|
|
149
|
+
bio = float(np.sum(S[bio_mask, :]))
|
|
150
|
+
total = anthro + bio
|
|
151
|
+
if total == 0:
|
|
152
|
+
return float("nan")
|
|
153
|
+
return (bio - anthro) / total
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def compute_entropy(y: np.ndarray, sr: int, n_fft: int = 1024,
|
|
157
|
+
hop: int = 512) -> tuple[float, float, float]:
|
|
158
|
+
"""Temporal entropy (Ht), spectral entropy (Hf), total entropy H = Ht × Hf.
|
|
159
|
+
After Sueur et al. (2008).
|
|
160
|
+
"""
|
|
161
|
+
# Temporal entropy
|
|
162
|
+
env = np.abs(y)
|
|
163
|
+
env_sum = env.sum()
|
|
164
|
+
if env_sum == 0:
|
|
165
|
+
return float("nan"), float("nan"), float("nan")
|
|
166
|
+
env_norm = env / env_sum
|
|
167
|
+
env_norm[env_norm == 0] = np.finfo(float).eps
|
|
168
|
+
Ht = float(-np.sum(env_norm * np.log(env_norm)) / math.log(len(env_norm)))
|
|
169
|
+
|
|
170
|
+
# Spectral entropy
|
|
171
|
+
S = np.abs(librosa.stft(y, n_fft=n_fft, hop_length=hop))
|
|
172
|
+
mean_spec = np.mean(S, axis=1)
|
|
173
|
+
sp_sum = mean_spec.sum()
|
|
174
|
+
if sp_sum == 0:
|
|
175
|
+
return Ht, float("nan"), float("nan")
|
|
176
|
+
sp_norm = mean_spec / sp_sum
|
|
177
|
+
sp_norm[sp_norm == 0] = np.finfo(float).eps
|
|
178
|
+
Hf = float(-np.sum(sp_norm * np.log(sp_norm)) / math.log(len(sp_norm)))
|
|
179
|
+
|
|
180
|
+
H = Ht * Hf
|
|
181
|
+
return Ht, Hf, H
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def process_file(fpath: Path, freq_min: float, freq_max: float) -> dict | None:
|
|
185
|
+
"""Load audio and compute all indices. Returns dict or None on error."""
|
|
186
|
+
try:
|
|
187
|
+
y, sr = librosa.load(str(fpath), sr=None, mono=True)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.warning("Nao foi possivel carregar %s: %s", fpath.name, e)
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
freq_max_use = min(freq_max, sr / 2)
|
|
193
|
+
n_fft = 1024
|
|
194
|
+
hop = 512
|
|
195
|
+
|
|
196
|
+
aci = compute_aci(y, sr, n_fft, hop, freq_min, freq_max_use)
|
|
197
|
+
bi = compute_bi(y, sr, n_fft, hop,
|
|
198
|
+
bio_min=max(freq_min, 2000),
|
|
199
|
+
bio_max=min(freq_max_use, 8000))
|
|
200
|
+
ndsi = compute_ndsi(y, sr, n_fft, hop,
|
|
201
|
+
anthro_min=max(freq_min, 200),
|
|
202
|
+
anthro_max=2000,
|
|
203
|
+
bio_min=2000,
|
|
204
|
+
bio_max=min(freq_max_use, 8000))
|
|
205
|
+
Ht, Hf, H = compute_entropy(y, sr, n_fft, hop)
|
|
206
|
+
|
|
207
|
+
ts = parse_timestamp(fpath.name)
|
|
208
|
+
return {
|
|
209
|
+
"file": fpath.name,
|
|
210
|
+
"datetime": ts.strftime("%Y-%m-%d %H:%M:%S") if ts else "",
|
|
211
|
+
"date": ts.strftime("%Y-%m-%d") if ts else "",
|
|
212
|
+
"hour": ts.hour if ts else -1,
|
|
213
|
+
"ACI": round(aci, 2),
|
|
214
|
+
"BI": round(bi, 2),
|
|
215
|
+
"NDSI": round(ndsi, 4),
|
|
216
|
+
"Ht": round(Ht, 4),
|
|
217
|
+
"Hf": round(Hf, 4),
|
|
218
|
+
"H": round(H, 4),
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def write_summary(rows: list[dict], output_dir: Path) -> None:
|
|
223
|
+
"""Write per-hour summary (mean ± SD) for each index."""
|
|
224
|
+
from collections import defaultdict
|
|
225
|
+
hour_data = defaultdict(lambda: {k: [] for k in ("ACI", "BI", "NDSI", "H")})
|
|
226
|
+
for r in rows:
|
|
227
|
+
if r["hour"] >= 0:
|
|
228
|
+
for idx in ("ACI", "BI", "NDSI", "H"):
|
|
229
|
+
v = r[idx]
|
|
230
|
+
if not math.isnan(v):
|
|
231
|
+
hour_data[r["hour"]][idx].append(v)
|
|
232
|
+
|
|
233
|
+
path = output_dir / "indices_summary.csv"
|
|
234
|
+
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
235
|
+
writer = csv.writer(f)
|
|
236
|
+
writer.writerow(["hour", "ACI_mean", "ACI_sd", "BI_mean", "BI_sd",
|
|
237
|
+
"NDSI_mean", "NDSI_sd", "H_mean", "H_sd", "n"])
|
|
238
|
+
for h in sorted(hour_data.keys()):
|
|
239
|
+
row = [h]
|
|
240
|
+
for idx in ("ACI", "BI", "NDSI", "H"):
|
|
241
|
+
vals = hour_data[h][idx]
|
|
242
|
+
if vals:
|
|
243
|
+
row += [round(sum(vals) / len(vals), 3),
|
|
244
|
+
round(math.sqrt(sum((v - sum(vals)/len(vals))**2
|
|
245
|
+
for v in vals) / max(len(vals)-1, 1)), 3)]
|
|
246
|
+
else:
|
|
247
|
+
row += ["", ""]
|
|
248
|
+
row.append(sum(len(hour_data[h][k]) for k in ("ACI",)) // 1) # n per hour
|
|
249
|
+
writer.writerow(row)
|
|
250
|
+
logger.info("Resumo escrito: %s", path)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def write_heatmap(rows: list[dict], output_dir: Path) -> None:
|
|
254
|
+
"""Produce a simple heatmap PNG of ACI by date × hour."""
|
|
255
|
+
try:
|
|
256
|
+
import matplotlib
|
|
257
|
+
matplotlib.use("Agg")
|
|
258
|
+
import matplotlib.pyplot as plt
|
|
259
|
+
except ImportError:
|
|
260
|
+
logger.warning("matplotlib nao disponivel; pulando heatmap")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
dated = [r for r in rows if r["date"] and r["hour"] >= 0]
|
|
264
|
+
if not dated:
|
|
265
|
+
logger.warning("Sem timestamps validos; heatmap nao gerado")
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
dates = sorted(set(r["date"] for r in dated))
|
|
269
|
+
hours = list(range(24))
|
|
270
|
+
|
|
271
|
+
from collections import defaultdict
|
|
272
|
+
grid = defaultdict(lambda: defaultdict(list))
|
|
273
|
+
for r in dated:
|
|
274
|
+
if not math.isnan(r["ACI"]):
|
|
275
|
+
grid[r["date"]][r["hour"]].append(r["ACI"])
|
|
276
|
+
|
|
277
|
+
matrix = np.full((len(dates), 24), np.nan)
|
|
278
|
+
for i, d in enumerate(dates):
|
|
279
|
+
for h in hours:
|
|
280
|
+
vals = grid[d][h]
|
|
281
|
+
if vals:
|
|
282
|
+
matrix[i, h] = sum(vals) / len(vals)
|
|
283
|
+
|
|
284
|
+
fig, ax = plt.subplots(figsize=(12, max(3, len(dates) * 0.35 + 2)))
|
|
285
|
+
im = ax.imshow(matrix, aspect="auto", cmap="magma",
|
|
286
|
+
interpolation="nearest",
|
|
287
|
+
extent=[-0.5, 23.5, len(dates) - 0.5, -0.5])
|
|
288
|
+
ax.set_xlabel("Hour of day")
|
|
289
|
+
ax.set_yticks(range(len(dates)))
|
|
290
|
+
ax.set_yticklabels(dates, fontsize=7)
|
|
291
|
+
ax.set_title("Acoustic Complexity Index (ACI) — date × hour heatmap")
|
|
292
|
+
plt.colorbar(im, ax=ax, label="ACI")
|
|
293
|
+
plt.tight_layout()
|
|
294
|
+
path = output_dir / "soundscape_plot.png"
|
|
295
|
+
fig.savefig(path, dpi=150)
|
|
296
|
+
plt.close(fig)
|
|
297
|
+
logger.info("Heatmap salvo: %s", path)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def main():
|
|
301
|
+
args = parse_args()
|
|
302
|
+
audio_dir = Path(args.audio_dir)
|
|
303
|
+
output_dir = Path(args.output_dir)
|
|
304
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
|
|
306
|
+
# ── Input precondition checks ────────────────────────────────────────────
|
|
307
|
+
if not audio_dir.is_dir():
|
|
308
|
+
logger.error(
|
|
309
|
+
"Input nao encontrado: %s\n Causa provavel: caminho incorreto ou diretorio nao montado\n Skill anterior: [nenhuma — etapa inicial]",
|
|
310
|
+
audio_dir,
|
|
311
|
+
)
|
|
312
|
+
sys.exit(1)
|
|
313
|
+
|
|
314
|
+
log_decision("resolution_min", args.resolution_min,
|
|
315
|
+
"resolucao de agregacao temporal em minutos")
|
|
316
|
+
log_decision("freq_min", args.freq_min,
|
|
317
|
+
"frequencia minima de analise em Hz")
|
|
318
|
+
log_decision("freq_max", args.freq_max,
|
|
319
|
+
"frequencia maxima de analise em Hz; limitado pela taxa de amostragem")
|
|
320
|
+
|
|
321
|
+
log_step(1, "Descobrindo arquivos de audio")
|
|
322
|
+
files = sorted(
|
|
323
|
+
p for ext in ("*.wav", "*.WAV", "*.flac", "*.FLAC")
|
|
324
|
+
for p in audio_dir.rglob(ext)
|
|
325
|
+
)
|
|
326
|
+
if not files:
|
|
327
|
+
logger.error(
|
|
328
|
+
"Nenhum arquivo de audio encontrado em %s\n Causa provavel: diretorio vazio ou extensoes nao reconhecidas\n Skill anterior: [nenhuma]",
|
|
329
|
+
audio_dir,
|
|
330
|
+
)
|
|
331
|
+
sys.exit(1)
|
|
332
|
+
logger.info("Encontrados %d arquivos de audio", len(files))
|
|
333
|
+
|
|
334
|
+
log_step(2, "Computando indices acusticos por arquivo")
|
|
335
|
+
rows = []
|
|
336
|
+
for i, fpath in enumerate(files, 1):
|
|
337
|
+
logger.info(" [%d/%d] %s", i, len(files), fpath.name)
|
|
338
|
+
try:
|
|
339
|
+
result = process_file(fpath, args.freq_min, args.freq_max)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
logger.error("Unexpected error in process_file for %s: %s", fpath.name, e)
|
|
342
|
+
raise
|
|
343
|
+
if result:
|
|
344
|
+
rows.append(result)
|
|
345
|
+
|
|
346
|
+
if not rows:
|
|
347
|
+
logger.error(
|
|
348
|
+
"Nenhum arquivo processado com sucesso\n Causa provavel: formato de audio incompativel ou intervalo de frequencia invalido\n Skill anterior: [nenhuma]"
|
|
349
|
+
)
|
|
350
|
+
sys.exit(1)
|
|
351
|
+
logger.info("%d de %d arquivos processados com sucesso", len(rows), len(files))
|
|
352
|
+
|
|
353
|
+
log_step(3, "Escrevendo CSV de serie temporal")
|
|
354
|
+
# Write timeseries
|
|
355
|
+
ts_path = output_dir / "acoustic_indices_timeseries.csv"
|
|
356
|
+
fieldnames = ["file", "datetime", "date", "hour",
|
|
357
|
+
"ACI", "BI", "NDSI", "Ht", "Hf", "H"]
|
|
358
|
+
with open(ts_path, "w", newline="", encoding="utf-8") as f:
|
|
359
|
+
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
360
|
+
writer.writeheader()
|
|
361
|
+
writer.writerows(rows)
|
|
362
|
+
logger.info("Serie temporal escrita: %s (%d linhas)", ts_path, len(rows))
|
|
363
|
+
|
|
364
|
+
log_step(4, "Escrevendo resumo por hora do dia")
|
|
365
|
+
write_summary(rows, output_dir)
|
|
366
|
+
|
|
367
|
+
log_step(5, "Gerando heatmap de paisagem sonora")
|
|
368
|
+
write_heatmap(rows, output_dir)
|
|
369
|
+
|
|
370
|
+
logger.info("Computacao de indices acusticos concluida")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
374
|
+
main()
|