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.
Files changed (217) hide show
  1. package/AGENT_CONTEXT.md +191 -0
  2. package/CATALOG.md +329 -0
  3. package/LICENSE +692 -0
  4. package/README.md +347 -0
  5. package/bin/install.mjs +168 -0
  6. package/docs/comparison-with-alternatives.md +38 -0
  7. package/docs/global-examples-index.md +103 -0
  8. package/docs/repository-statistics.md +101 -0
  9. package/docs/theoretical-foundations.md +188 -0
  10. package/environment.yaml +106 -0
  11. package/examples/community/arctic_tundra_vegetation_example.md +247 -0
  12. package/examples/community/bird_landuse_example.md +63 -0
  13. package/examples/community/phytoplankton_reservoir_example.md +60 -0
  14. package/examples/community/reef_fish_indopacific_example.md +221 -0
  15. package/examples/impact/baci_road_example.md +57 -0
  16. package/examples/impact/ecosystem_services_atlantic_forest.md +83 -0
  17. package/examples/impact/forest_loss_borneo_timeseries_example.md +225 -0
  18. package/examples/occupancy/puma_camera_example.md +61 -0
  19. package/examples/occupancy/snow_leopard_himalayas_example.md +204 -0
  20. package/examples/reproducible/whittaker_biome_sdm_example.md +406 -0
  21. package/examples/sdm/anteater_cerrado_example.md +69 -0
  22. package/examples/sdm/jaguar_amazon_example.md +80 -0
  23. package/examples/sdm/koala_climate_change_example.md +170 -0
  24. package/examples/sdm/wolf_recolonization_europe_example.md +193 -0
  25. package/package.json +43 -0
  26. package/renv.lock +194 -0
  27. package/skills/SKILL_INDEX.json +1020 -0
  28. package/skills/acoustic-monitoring/SKILL.md +163 -0
  29. package/skills/acoustic-monitoring/examples/example-prompts.md +100 -0
  30. package/skills/acoustic-monitoring/examples/temperate_forest_birds_example.md +285 -0
  31. package/skills/acoustic-monitoring/resources/acoustic-indices-reference.md +93 -0
  32. package/skills/acoustic-monitoring/resources/soundscape-ecology-guide.md +90 -0
  33. package/skills/acoustic-monitoring/resources/species-id-tools-comparison.md +89 -0
  34. package/skills/acoustic-monitoring/scripts/batch_species_detection.py +360 -0
  35. package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.R +235 -0
  36. package/skills/acoustic-monitoring/scripts/compute_acoustic_indices.py +374 -0
  37. package/skills/biostatistics-workbench/SKILL.md +140 -0
  38. package/skills/biostatistics-workbench/examples/example-prompts.md +39 -0
  39. package/skills/biostatistics-workbench/resources/effect-size-reference.md +81 -0
  40. package/skills/biostatistics-workbench/resources/glm-family-link-reference.md +47 -0
  41. package/skills/biostatistics-workbench/resources/test-selection-guide.md +93 -0
  42. package/skills/biostatistics-workbench/scripts/glm_pipeline.R +78 -0
  43. package/skills/biostatistics-workbench/scripts/glm_pipeline.py +210 -0
  44. package/skills/camera-trap-processing/SKILL.md +159 -0
  45. package/skills/camera-trap-processing/examples/example-prompts.md +103 -0
  46. package/skills/camera-trap-processing/examples/leopard_serengeti_example.md +231 -0
  47. package/skills/camera-trap-processing/resources/activity-patterns-reference.md +113 -0
  48. package/skills/camera-trap-processing/resources/camtrapR-workflow-guide.md +130 -0
  49. package/skills/camera-trap-processing/resources/detection-event-definition-guide.md +89 -0
  50. package/skills/camera-trap-processing/scripts/estimate_activity.R +169 -0
  51. package/skills/camera-trap-processing/scripts/process_camtrap_data.R +179 -0
  52. package/skills/camera-trap-processing/scripts/process_camtrap_data.py +192 -0
  53. package/skills/community-ecology-ordination/SKILL.md +133 -0
  54. package/skills/community-ecology-ordination/examples/example-prompts.md +35 -0
  55. package/skills/community-ecology-ordination/resources/dissimilarity-metric-guide.md +53 -0
  56. package/skills/community-ecology-ordination/resources/nmds-interpretation-guide.md +104 -0
  57. package/skills/community-ecology-ordination/scripts/__pycache__/community_analysis.cpython-311.pyc +0 -0
  58. package/skills/community-ecology-ordination/scripts/community_analysis.R +143 -0
  59. package/skills/community-ecology-ordination/scripts/community_analysis.py +231 -0
  60. package/skills/ecological-data-foundation/SKILL.md +129 -0
  61. package/skills/ecological-data-foundation/examples/example-prompts.md +40 -0
  62. package/skills/ecological-data-foundation/resources/coordinate-cleaning-flags.md +66 -0
  63. package/skills/ecological-data-foundation/resources/darwin-core-glossary.md +91 -0
  64. package/skills/ecological-data-foundation/resources/data-citation-guide.md +265 -0
  65. package/skills/ecological-data-foundation/resources/gbif-data-citation-guide.md +193 -0
  66. package/skills/ecological-data-foundation/resources/qa-checklist.md +83 -0
  67. package/skills/ecological-data-foundation/scripts/__pycache__/clean_occurrences.cpython-311.pyc +0 -0
  68. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_ebird.cpython-311.pyc +0 -0
  69. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_inat.cpython-311.pyc +0 -0
  70. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_iucn.cpython-311.pyc +0 -0
  71. package/skills/ecological-data-foundation/scripts/__pycache__/download_from_obis.cpython-311.pyc +0 -0
  72. package/skills/ecological-data-foundation/scripts/clean_occurrences.R +230 -0
  73. package/skills/ecological-data-foundation/scripts/clean_occurrences.py +268 -0
  74. package/skills/ecological-data-foundation/scripts/download_from_ebird.R +251 -0
  75. package/skills/ecological-data-foundation/scripts/download_from_ebird.py +364 -0
  76. package/skills/ecological-data-foundation/scripts/download_from_gbif.R +315 -0
  77. package/skills/ecological-data-foundation/scripts/download_from_gbif.py +407 -0
  78. package/skills/ecological-data-foundation/scripts/download_from_inat.R +238 -0
  79. package/skills/ecological-data-foundation/scripts/download_from_inat.py +304 -0
  80. package/skills/ecological-data-foundation/scripts/download_from_iucn.R +273 -0
  81. package/skills/ecological-data-foundation/scripts/download_from_iucn.py +344 -0
  82. package/skills/ecological-data-foundation/scripts/download_from_obis.R +248 -0
  83. package/skills/ecological-data-foundation/scripts/download_from_obis.py +318 -0
  84. package/skills/ecological-impact-assessment/SKILL.md +123 -0
  85. package/skills/ecological-impact-assessment/examples/example-prompts.md +32 -0
  86. package/skills/ecological-impact-assessment/resources/baci-design-guide.md +55 -0
  87. package/skills/ecological-impact-assessment/resources/fragmentation-metrics-reference.md +86 -0
  88. package/skills/ecological-impact-assessment/resources/pressure-index-template.md +78 -0
  89. package/skills/ecological-impact-assessment/resources/study-design-guide.md +168 -0
  90. package/skills/ecological-impact-assessment/scripts/baci_analysis.R +161 -0
  91. package/skills/ecological-impact-assessment/scripts/fragmentation_analysis.py +141 -0
  92. package/skills/ecological-impact-assessment/scripts/power_analysis_baci.R +274 -0
  93. package/skills/ecosystem-services-assessment/SKILL.md +125 -0
  94. package/skills/ecosystem-services-assessment/examples/example-prompts.md +24 -0
  95. package/skills/ecosystem-services-assessment/resources/es-indicator-reference.md +45 -0
  96. package/skills/ecosystem-services-assessment/resources/invest-parameter-guide.md +86 -0
  97. package/skills/ecosystem-services-assessment/resources/rusle-coefficients.md +88 -0
  98. package/skills/ecosystem-services-assessment/scripts/__pycache__/compute_es.cpython-311.pyc +0 -0
  99. package/skills/ecosystem-services-assessment/scripts/compute_es.py +189 -0
  100. package/skills/ecosystem-services-assessment/scripts/tradeoff_analysis.R +161 -0
  101. package/skills/environmental-time-series/SKILL.md +125 -0
  102. package/skills/environmental-time-series/examples/example-prompts.md +33 -0
  103. package/skills/environmental-time-series/resources/anomaly-indices-reference.md +88 -0
  104. package/skills/environmental-time-series/resources/bfast-parameter-guide.md +69 -0
  105. package/skills/environmental-time-series/scripts/__pycache__/recovery_trajectory.cpython-311.pyc +0 -0
  106. package/skills/environmental-time-series/scripts/__pycache__/trend_analysis.cpython-311.pyc +0 -0
  107. package/skills/environmental-time-series/scripts/recovery_trajectory.R +305 -0
  108. package/skills/environmental-time-series/scripts/recovery_trajectory.py +178 -0
  109. package/skills/environmental-time-series/scripts/trend_analysis.R +192 -0
  110. package/skills/environmental-time-series/scripts/trend_analysis.py +184 -0
  111. package/skills/geoprocessing-for-ecology/SKILL.md +123 -0
  112. package/skills/geoprocessing-for-ecology/examples/example-prompts.md +32 -0
  113. package/skills/geoprocessing-for-ecology/resources/crs-reference.md +62 -0
  114. package/skills/geoprocessing-for-ecology/resources/global-predictor-sources.md +331 -0
  115. package/skills/geoprocessing-for-ecology/resources/resampling-methods.md +57 -0
  116. package/skills/geoprocessing-for-ecology/scripts/__pycache__/download_predictors.cpython-311.pyc +0 -0
  117. package/skills/geoprocessing-for-ecology/scripts/download_predictors.R +239 -0
  118. package/skills/geoprocessing-for-ecology/scripts/download_predictors.py +379 -0
  119. package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.R +224 -0
  120. package/skills/geoprocessing-for-ecology/scripts/stack_and_extract.py +172 -0
  121. package/skills/landscape-connectivity/SKILL.md +170 -0
  122. package/skills/landscape-connectivity/examples/example-prompts.md +96 -0
  123. package/skills/landscape-connectivity/examples/jaguar_mesoamerica_corridor_example.md +271 -0
  124. package/skills/landscape-connectivity/resources/circuitscape-parameter-guide.md +155 -0
  125. package/skills/landscape-connectivity/resources/graph-theory-for-ecology.md +134 -0
  126. package/skills/landscape-connectivity/resources/resistance-surface-guide.md +141 -0
  127. package/skills/landscape-connectivity/scripts/connectivity_analysis.py +387 -0
  128. package/skills/landscape-connectivity/scripts/connectivity_metrics.R +274 -0
  129. package/skills/landscape-connectivity/scripts/resistance_surface.R +239 -0
  130. package/skills/model-validation-and-uncertainty/SKILL.md +131 -0
  131. package/skills/model-validation-and-uncertainty/examples/example-prompts.md +30 -0
  132. package/skills/model-validation-and-uncertainty/resources/extrapolation-risk-guide.md +236 -0
  133. package/skills/model-validation-and-uncertainty/resources/metric-selection-guide.md +52 -0
  134. package/skills/model-validation-and-uncertainty/resources/threshold-selection-guide.md +64 -0
  135. package/skills/model-validation-and-uncertainty/scripts/__pycache__/validate_model.cpython-311.pyc +0 -0
  136. package/skills/model-validation-and-uncertainty/scripts/extrapolation_risk.R +315 -0
  137. package/skills/model-validation-and-uncertainty/scripts/validate_model.py +226 -0
  138. package/skills/model-validation-and-uncertainty/scripts/validate_sdm.R +162 -0
  139. package/skills/occupancy-and-detection/SKILL.md +126 -0
  140. package/skills/occupancy-and-detection/examples/example-prompts.md +33 -0
  141. package/skills/occupancy-and-detection/resources/detection-history-format.md +100 -0
  142. package/skills/occupancy-and-detection/resources/occupancy-study-design.md +47 -0
  143. package/skills/occupancy-and-detection/scripts/__pycache__/occupancy_analysis.cpython-311.pyc +0 -0
  144. package/skills/occupancy-and-detection/scripts/occupancy_analysis.R +160 -0
  145. package/skills/occupancy-and-detection/scripts/occupancy_analysis.py +159 -0
  146. package/skills/population-viability-analysis/SKILL.md +161 -0
  147. package/skills/population-viability-analysis/examples/african_elephant_pva_example.md +266 -0
  148. package/skills/population-viability-analysis/examples/example-prompts.md +95 -0
  149. package/skills/population-viability-analysis/resources/extinction-risk-thresholds.md +128 -0
  150. package/skills/population-viability-analysis/resources/matrix-model-guide.md +139 -0
  151. package/skills/population-viability-analysis/resources/sensitivity-elasticity-reference.md +182 -0
  152. package/skills/population-viability-analysis/scripts/matrix_pva.R +258 -0
  153. package/skills/population-viability-analysis/scripts/pva_analysis.py +442 -0
  154. package/skills/population-viability-analysis/scripts/stochastic_pva.R +353 -0
  155. package/skills/predictive-modeling-best-practices/SKILL.md +136 -0
  156. package/skills/predictive-modeling-best-practices/examples/example-prompts.md +58 -0
  157. package/skills/predictive-modeling-best-practices/resources/collinearity-decision-tree.md +65 -0
  158. package/skills/predictive-modeling-best-practices/resources/sampling-bias-correction.md +267 -0
  159. package/skills/predictive-modeling-best-practices/resources/spatial-cv-guide.md +73 -0
  160. package/skills/predictive-modeling-best-practices/scripts/__pycache__/spatial_cv.cpython-311.pyc +0 -0
  161. package/skills/predictive-modeling-best-practices/scripts/collinearity_check.R +112 -0
  162. package/skills/predictive-modeling-best-practices/scripts/spatial_cv.py +182 -0
  163. package/skills/reproducible-ecology-pipeline/SKILL.md +139 -0
  164. package/skills/reproducible-ecology-pipeline/examples/example-prompts.md +35 -0
  165. package/skills/reproducible-ecology-pipeline/resources/directory-structure-template.md +94 -0
  166. package/skills/reproducible-ecology-pipeline/resources/params-yaml-template.yaml +84 -0
  167. package/skills/reproducible-ecology-pipeline/resources/reproducibility-checklist-template.md +66 -0
  168. package/skills/reproducible-ecology-pipeline/scripts/generate_file_manifest.py +110 -0
  169. package/skills/reproducible-ecology-pipeline/scripts/init_project.sh +53 -0
  170. package/skills/spatial-prioritization/SKILL.md +162 -0
  171. package/skills/spatial-prioritization/examples/biodiversity_hotspot_prioritization_example.md +289 -0
  172. package/skills/spatial-prioritization/examples/example-prompts.md +93 -0
  173. package/skills/spatial-prioritization/resources/cost-surface-reference.md +130 -0
  174. package/skills/spatial-prioritization/resources/marxan-vs-prioritizr-comparison.md +125 -0
  175. package/skills/spatial-prioritization/resources/prioritizr-formulation-guide.md +188 -0
  176. package/skills/spatial-prioritization/resources/representation-targets-guide.md +186 -0
  177. package/skills/spatial-prioritization/scripts/prioritization_sensitivity.R +320 -0
  178. package/skills/spatial-prioritization/scripts/run_prioritization.R +336 -0
  179. package/skills/species-distribution-modeling/SKILL.md +139 -0
  180. package/skills/species-distribution-modeling/examples/example-prompts.md +36 -0
  181. package/skills/species-distribution-modeling/resources/algorithm-comparison.md +25 -0
  182. package/skills/species-distribution-modeling/resources/calibration-area-guide.md +71 -0
  183. package/skills/species-distribution-modeling/resources/climate-scenario-preparation.md +170 -0
  184. package/skills/species-distribution-modeling/resources/maxent-calibration-guide.md +211 -0
  185. package/skills/species-distribution-modeling/resources/sdm-checklist.md +37 -0
  186. package/skills/species-distribution-modeling/scripts/predict_distribution.R +236 -0
  187. package/skills/species-distribution-modeling/scripts/predict_distribution.py +286 -0
  188. package/skills/species-distribution-modeling/scripts/prepare_future_layers.R +351 -0
  189. package/skills/species-distribution-modeling/scripts/project_scenarios.R +220 -0
  190. package/skills/species-distribution-modeling/scripts/run_ensemble_sdm.R +99 -0
  191. package/skills/species-distribution-modeling/scripts/sdm_pipeline.py +318 -0
  192. package/skills/species-distribution-modeling/scripts/tune_maxnet.R +344 -0
  193. package/templates/SKILL_TEMPLATE.md +225 -0
  194. package/templates/checklists/data-submission-checklist.md +38 -0
  195. package/templates/checklists/post-analysis-checklist.md +55 -0
  196. package/templates/checklists/pre-analysis-checklist.md +31 -0
  197. package/templates/prompts/debug-skill.md +47 -0
  198. package/templates/prompts/invoke-skill.md +34 -0
  199. package/templates/prompts/invoke-workflow.md +45 -0
  200. package/templates/reports/technical-report-template.md +80 -0
  201. package/templates/scripts/logger_setup.R +79 -0
  202. package/templates/scripts/logger_setup.py +119 -0
  203. package/templates/scripts/params_loader.R +28 -0
  204. package/templates/scripts/params_loader.py +38 -0
  205. package/workflows/analyze-community-structure/WORKFLOW.md +72 -0
  206. package/workflows/analyze-environmental-change/WORKFLOW.md +73 -0
  207. package/workflows/assess-ecological-impact/WORKFLOW.md +75 -0
  208. package/workflows/assess-ecosystem-services/WORKFLOW.md +68 -0
  209. package/workflows/assess-landscape-connectivity/WORKFLOW.md +84 -0
  210. package/workflows/build-fire-risk-map/WORKFLOW.md +79 -0
  211. package/workflows/produce-technical-report/WORKFLOW.md +113 -0
  212. package/workflows/run-camera-trap-occupancy/WORKFLOW.md +87 -0
  213. package/workflows/run-conservation-prioritization/WORKFLOW.md +89 -0
  214. package/workflows/run-multispecies-screening/WORKFLOW.md +197 -0
  215. package/workflows/run-occupancy-analysis/WORKFLOW.md +74 -0
  216. package/workflows/run-population-viability/WORKFLOW.md +90 -0
  217. 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()