Better cli docs

This commit is contained in:
mbsantiago 2026-03-28 13:25:15 +00:00
parent 7277151f33
commit e4bbde9995
27 changed files with 481 additions and 143 deletions

View File

@ -41,6 +41,8 @@ html_theme = "sphinx_book_theme"
html_static_path = ["_static"] html_static_path = ["_static"]
html_theme_options = { html_theme_options = {
"home_page_in_toc": True, "home_page_in_toc": True,
"show_navbar_depth": 2,
"show_toc_level": 2,
} }
intersphinx_mapping = { intersphinx_mapping = {
@ -58,7 +60,7 @@ intersphinx_mapping = {
} }
# -- Options for autodoc ------------------------------------------------------ # -- Options for autodoc ------------------------------------------------------
autosummary_generate = True autosummary_generate = False
autosummary_imported_members = True autosummary_imported_members = True
autodoc_default_options = { autodoc_default_options = {
@ -70,3 +72,7 @@ autodoc_default_options = {
"show-inheritance": True, "show-inheritance": True,
"module-first": True, "module-first": True,
} }
numpydoc_show_class_members = False
numpydoc_show_inherited_class_members = False
numpydoc_class_members_toctree = False

View File

@ -80,4 +80,4 @@ pip install batdetect2
- Run your first detection workflow: - Run your first detection workflow:
{doc}`tutorials/run-inference-on-folder` {doc}`tutorials/run-inference-on-folder`
- For practical task recipes, go to {doc}`how_to/index` - For practical task recipes, go to {doc}`how_to/index`
- For command and option details, go to {doc}`reference/cli` - For command and option details, go to {doc}`reference/cli/index`

View File

@ -27,4 +27,4 @@ batdetect2 predict file_list \
- `--workers` to set data-loading parallelism. - `--workers` to set data-loading parallelism.
- `--format` to select output format. - `--format` to select output format.
For complete option details, see {doc}`../reference/cli`. For complete option details, see {doc}`../reference/cli/index`.

View File

@ -1,7 +0,0 @@
CLI reference
=============
.. click:: batdetect2.cli:cli
:prog: batdetect2
:nested: full

View File

@ -0,0 +1,8 @@
Base command
============
The options on this page apply to all subcommands.
.. click:: batdetect2.cli:cli
:prog: batdetect2
:nested: none

View File

@ -0,0 +1,8 @@
Data command
============
Inspect and convert dataset config files.
.. click:: batdetect2.cli.data:data
:prog: batdetect2 data
:nested: full

View File

@ -0,0 +1,17 @@
Legacy detect command
=====================
.. warning::
``batdetect2 detect`` is a legacy compatibility command.
Prefer ``batdetect2 predict directory`` for new workflows.
Migration at a glance
---------------------
- Legacy: ``batdetect2 detect AUDIO_DIR ANN_DIR DETECTION_THRESHOLD``
- Current: ``batdetect2 predict directory MODEL_PATH AUDIO_DIR OUTPUT_PATH``
.. click:: batdetect2.cli.compat:detect
:prog: batdetect2 detect
:nested: none

View File

@ -0,0 +1,8 @@
Evaluate command
================
Evaluate a checkpoint against a configured test dataset.
.. click:: batdetect2.cli.evaluate:evaluate_command
:prog: batdetect2 evaluate
:nested: none

View File

@ -0,0 +1,18 @@
# CLI reference
Use this section like this:
1. Check `base` for global options shared by all commands.
2. Pick the command page you need (`predict`, `data`, `train`, or `evaluate`).
3. Use `detect_legacy` only if you are maintaining old workflows.
```{toctree}
:maxdepth: 1
base
predict
data
train
evaluate
detect_legacy
```

View File

@ -0,0 +1,8 @@
Predict command
===============
Run model inference from a directory, a file list, or a dataset.
.. click:: batdetect2.cli.inference:predict
:prog: batdetect2 predict
:nested: full

View File

@ -0,0 +1,8 @@
Train command
=============
Train a model from dataset configs or fine-tune from a checkpoint.
.. click:: batdetect2.cli.train:train_command
:prog: batdetect2 train
:nested: none

View File

@ -1,7 +0,0 @@
# Config Reference
```{eval-rst}
.. automodule:: batdetect2.config
:members:
:inherited-members: pydantic.BaseModel
```

View File

@ -0,0 +1,5 @@
Config reference
================
.. automodule:: batdetect2.config
:members:

View File

@ -6,7 +6,7 @@ configuration, and data structures.
```{toctree} ```{toctree}
:maxdepth: 1 :maxdepth: 1
cli cli/index
configs configs
targets targets
``` ```

View File

@ -1,6 +0,0 @@
# Targets Reference
```{eval-rst}
.. automodule:: batdetect2.targets
:members:
```

View File

@ -0,0 +1,5 @@
Targets reference
=================
.. automodule:: batdetect2.targets
:members:

View File

@ -1,6 +1,6 @@
## Defining Target Geometry: Mapping Sound Event Regions # Defining Target Geometry: Mapping Sound Event Regions
### Introduction ## Introduction
In the previous steps of defining targets, we focused on determining _which_ sound events are relevant (`filtering`), _what_ descriptive tags they should have (`transform`), and _which category_ they belong to (`classes`). In the previous steps of defining targets, we focused on determining _which_ sound events are relevant (`filtering`), _what_ descriptive tags they should have (`transform`), and _which category_ they belong to (`classes`).
However, for the model to learn effectively, it also needs to know **where** in the spectrogram each sound event is located and approximately **how large** it is. However, for the model to learn effectively, it also needs to know **where** in the spectrogram each sound event is located and approximately **how large** it is.
@ -10,7 +10,7 @@ This ROI contains detailed spatial information (start/end time, low/high frequen
This section explains how BatDetect2 converts the geometric ROI from your annotations into the specific positional and size information used as targets during model training. This section explains how BatDetect2 converts the geometric ROI from your annotations into the specific positional and size information used as targets during model training.
### From ROI to Model Targets: Position & Size ## From ROI to Model Targets: Position & Size
BatDetect2 does not directly predict a full bounding box. BatDetect2 does not directly predict a full bounding box.
Instead, it is trained to predict: Instead, it is trained to predict:
@ -21,7 +21,7 @@ Instead, it is trained to predict:
This step defines _how_ BatDetect2 calculates this specific reference point and these numerical size values from the original annotation's bounding box. This step defines _how_ BatDetect2 calculates this specific reference point and these numerical size values from the original annotation's bounding box.
It also handles the reverse process converting predicted positions and sizes back into bounding boxes for visualization or analysis. It also handles the reverse process converting predicted positions and sizes back into bounding boxes for visualization or analysis.
### Configuring the ROI Mapping ## Configuring the ROI Mapping
You can control how this conversion happens through settings in your configuration file (e.g., your main `.yaml` file). You can control how this conversion happens through settings in your configuration file (e.g., your main `.yaml` file).
These settings are usually placed within the main `targets:` configuration block, under a specific `roi:` key. These settings are usually placed within the main `targets:` configuration block, under a specific `roi:` key.
@ -74,12 +74,12 @@ targets: # Top-level key for target definition
frequency_scale: 0.00116 # e.g., Model predicts height relative to ~860Hz (or other model-specific scaling) frequency_scale: 0.00116 # e.g., Model predicts height relative to ~860Hz (or other model-specific scaling)
``` ```
### Decoding Size Predictions ## Decoding Size Predictions
These scaling factors (`time_scale`, `frequency_scale`) are also essential for interpreting the model's output correctly. These scaling factors (`time_scale`, `frequency_scale`) are also essential for interpreting the model's output correctly.
When the model predicts numerical values for width and height, BatDetect2 uses these same scales (in reverse) to convert those numbers back into physically meaningful durations (seconds) and bandwidths (Hz/kHz) when reconstructing bounding boxes from predictions. When the model predicts numerical values for width and height, BatDetect2 uses these same scales (in reverse) to convert those numbers back into physically meaningful durations (seconds) and bandwidths (Hz/kHz) when reconstructing bounding boxes from predictions.
### Outcome ## Outcome
By configuring the `roi` settings, you ensure that BatDetect2 consistently translates the geometric information from your annotations into the specific reference points and scaled size values required for training the model. By configuring the `roi` settings, you ensure that BatDetect2 consistently translates the geometric information from your annotations into the specific reference points and scaled size values required for training the model.
Using consistent scales that are appropriate for your data and potentially beneficial for training stability allows the model to effectively learn not just _what_ sound is present, but also _where_ it is located and _how large_ it is, and enables meaningful interpretation of the model's spatial and size predictions. Using consistent scales that are appropriate for your data and potentially beneficial for training stability allows the model to effectively learn not just _what_ sound is present, but also _where_ it is located and _how large_ it is, and enables meaningful interpretation of the model's spatial and size predictions.

View File

@ -1,6 +1,6 @@
## Bringing It All Together: The `Targets` Object # Bringing It All Together: The `Targets` Object
### Recap: Defining Your Target Strategy ## Recap: Defining Your Target Strategy
In the previous sections, we covered the sequential steps to precisely define what your BatDetect2 model should learn, specified within your configuration file: In the previous sections, we covered the sequential steps to precisely define what your BatDetect2 model should learn, specified within your configuration file:
@ -12,7 +12,7 @@ In the previous sections, we covered the sequential steps to precisely define wh
You define all these aspects within your configuration file (e.g., YAML), which holds the complete specification for your target definition strategy, typically under a main `targets:` key. You define all these aspects within your configuration file (e.g., YAML), which holds the complete specification for your target definition strategy, typically under a main `targets:` key.
### What is the `Targets` Object? ## What is the `Targets` Object?
While the configuration file specifies _what_ you want to happen, BatDetect2 needs an active component to actually _perform_ these steps. While the configuration file specifies _what_ you want to happen, BatDetect2 needs an active component to actually _perform_ these steps.
This is the role of the `Targets` object. This is the role of the `Targets` object.
@ -21,7 +21,7 @@ The `Targets` is an organized container that holds all the specific functions an
It's created directly from your configuration and provides methods to apply the **filtering**, **transformation**, **ROI mapping** (geometry to position/size and back), **class encoding**, and **class decoding** steps you defined. It's created directly from your configuration and provides methods to apply the **filtering**, **transformation**, **ROI mapping** (geometry to position/size and back), **class encoding**, and **class decoding** steps you defined.
It effectively bundles together all the target definition logic determined by your settings into a single, usable object. It effectively bundles together all the target definition logic determined by your settings into a single, usable object.
### How is it Created and Used? ## How is it Created and Used?
For most standard training workflows, you typically won't need to create or interact with the `Targets` object directly in Python code. For most standard training workflows, you typically won't need to create or interact with the `Targets` object directly in Python code.
BatDetect2 usually handles its creation automatically when you provide your main configuration file during training setup. BatDetect2 usually handles its creation automatically when you provide your main configuration file during training setup.
@ -50,7 +50,7 @@ targets_processor: TargetProtocol = load_targets(target_config_file)
# to be used internally by the training pipeline or for prediction processing. # to be used internally by the training pipeline or for prediction processing.
``` ```
### What Does the `Targets` Object Do? (Its Role) ## What Does the `Targets` Object Do? (Its Role)
Once created, the `targets_processor` object plays several vital roles within the BatDetect2 system: Once created, the `targets_processor` object plays several vital roles within the BatDetect2 system:
@ -71,7 +71,7 @@ Once created, the `targets_processor` object plays several vital roles within th
- `targets_processor.generic_class_tags`: The tags representing the generic class. - `targets_processor.generic_class_tags`: The tags representing the generic class.
- `targets_processor.dimension_names`: The names used for the size dimensions (e.g., `['width', 'height']`). - `targets_processor.dimension_names`: The names used for the size dimensions (e.g., `['width', 'height']`).
### Why is Understanding This Important? ## Why is Understanding This Important?
As a researcher using BatDetect2, your primary interaction is typically through the **configuration file**. As a researcher using BatDetect2, your primary interaction is typically through the **configuration file**.
The `Targets` object is the component that materializes your configurations. The `Targets` object is the component that materializes your configurations.
@ -84,7 +84,7 @@ Understanding its role can be important:
While standard training runs handle this object internally, the underlying functions for filtering, transforming, encoding, decoding, and ROI mapping are accessible or can be built individually. While standard training runs handle this object internally, the underlying functions for filtering, transforming, encoding, decoding, and ROI mapping are accessible or can be built individually.
This modular design provides the **flexibility to use or customize specific parts of the target definition workflow programmatically** for advanced analyses, integration tasks, or specialized data processing pipelines, should you need to go beyond the standard configuration-driven approach. This modular design provides the **flexibility to use or customize specific parts of the target definition workflow programmatically** for advanced analyses, integration tasks, or specialized data processing pipelines, should you need to go beyond the standard configuration-driven approach.
### Summary ## Summary
The `Targets` object encapsulates the entire configured target definition logic specified in your `TargetConfig` file. The `Targets` object encapsulates the entire configured target definition logic specified in your `TargetConfig` file.
It acts as the central component within BatDetect2 for applying filtering, tag transformation, ROI mapping (geometry to/from position/size), class encoding (for training preparation), and class/ROI decoding (for interpreting predictions). It acts as the central component within BatDetect2 for applying filtering, tag transformation, ROI mapping (geometry to/from position/size), class encoding (for training preparation), and class/ROI decoding (for interpreting predictions).

View File

@ -25,7 +25,7 @@ batdetect2 predict directory \
## What to do next ## What to do next
- Use {doc}`../how_to/tune-detection-threshold` to tune sensitivity. - Use {doc}`../how_to/tune-detection-threshold` to tune sensitivity.
- Use {doc}`../reference/cli` for full command options. - Use {doc}`../reference/cli/index` for full command options.
Note: this is the initial Phase 1 scaffold and will be expanded with a full, Note: this is the initial Phase 1 scaffold and will be expanded with a full,
validated end-to-end walkthrough. validated end-to-end walkthrough.

View File

@ -27,7 +27,11 @@ BatDetect2 - Detection and Classification
help="Increase verbosity. -v for INFO, -vv for DEBUG.", help="Increase verbosity. -v for INFO, -vv for DEBUG.",
) )
def cli(verbose: int = 0): def cli(verbose: int = 0):
"""BatDetect2 - Bat Call Detection and Classification.""" """Run the BatDetect2 CLI.
This command initializes logging and exposes subcommands for prediction,
training, evaluation, and dataset utilities.
"""
click.echo(INFO_STR) click.echo(INFO_STR)
enable_logging(verbose) enable_logging(verbose)

View File

@ -12,7 +12,13 @@ DEFAULT_MODEL_PATH = os.path.join(
) )
@cli.command() @cli.command(
short_help="Legacy detection command.",
epilog=(
"Deprecated workflow. Prefer `batdetect2 predict directory` for "
"new analyses."
),
)
@click.argument( @click.argument(
"audio_dir", "audio_dir",
type=click.Path(exists=True), type=click.Path(exists=True),
@ -68,7 +74,10 @@ def detect(
time_expansion_factor: int, time_expansion_factor: int,
**args, **args,
): ):
"""Detect bat calls in files in AUDIO_DIR and save predictions to ANN_DIR. """Legacy detection command for directory-based inference.
Detect bat calls in files in `AUDIO_DIR` and save predictions to
`ANN_DIR`.
DETECTION_THRESHOLD is the detection threshold. All predictions with a DETECTION_THRESHOLD is the detection threshold. All predictions with a
score below this threshold will be discarded. Values between 0 and 1. score below this threshold will be discarded. Values between 0 and 1.
@ -78,6 +87,11 @@ def detect(
Spaces in the input paths will throw an error. Wrap in quotes. Spaces in the input paths will throw an error. Wrap in quotes.
Input files should be short in duration e.g. < 30 seconds. Input files should be short in duration e.g. < 30 seconds.
Note
----
This command is kept for backwards compatibility. Prefer
`batdetect2 predict directory` for new workflows.
""" """
from batdetect2 import api from batdetect2 import api
from batdetect2.utils.detector_utils import save_results_to_file from batdetect2.utils.detector_utils import save_results_to_file
@ -132,7 +146,7 @@ def detect(
def print_config(config): def print_config(config):
"""Print the processing configuration.""" """Print the processing configuration values."""
click.echo("\nProcessing Configuration:") click.echo("\nProcessing Configuration:")
click.echo(f"Time Expansion Factor: {config.get('time_expansion')}") click.echo(f"Time Expansion Factor: {config.get('time_expansion')}")
click.echo(f"Detection Threshold: {config.get('detection_threshold')}") click.echo(f"Detection Threshold: {config.get('detection_threshold')}")

View File

@ -7,12 +7,12 @@ from batdetect2.cli.base import cli
__all__ = ["data"] __all__ = ["data"]
@cli.group() @cli.group(short_help="Inspect and convert datasets.")
def data(): def data():
"""Inspect and convert dataset configuration files.""" """Inspect and convert dataset configuration files."""
@data.command() @data.command(short_help="Print dataset summary information.")
@click.argument( @click.argument(
"dataset_config", "dataset_config",
type=click.Path(exists=True), type=click.Path(exists=True),
@ -20,17 +20,27 @@ def data():
@click.option( @click.option(
"--field", "--field",
type=str, type=str,
help="If the dataset info is in a nested field please specify here.", help=(
"Nested field name that contains dataset configuration. "
"Use this when the config is wrapped in a larger file."
),
) )
@click.option( @click.option(
"--targets", "--targets",
"targets_path", "targets_path",
type=click.Path(exists=True), type=click.Path(exists=True),
help=(
"Path to targets config file. If provided, a per-class summary "
"table is printed."
),
) )
@click.option( @click.option(
"--base-dir", "--base-dir",
type=click.Path(exists=True), type=click.Path(exists=True),
help="The base directory to which all recording and annotations paths are relative to.", help=(
"Base directory used to resolve relative recording and annotation "
"paths in the dataset config."
),
) )
def summary( def summary(
dataset_config: Path, dataset_config: Path,
@ -38,7 +48,11 @@ def summary(
targets_path: Path | None = None, targets_path: Path | None = None,
base_dir: Path | None = None, base_dir: Path | None = None,
): ):
"""Show annotation counts and optional class summary.""" """Show dataset size and optional class summary.
Prints the number of annotated clips. If `--targets` is provided, it also
prints a per-class summary table based on the configured targets.
"""
from batdetect2.data import compute_class_summary, load_dataset_from_config from batdetect2.data import compute_class_summary, load_dataset_from_config
from batdetect2.targets import load_targets from batdetect2.targets import load_targets
@ -62,7 +76,7 @@ def summary(
print(summary.to_markdown()) print(summary.to_markdown())
@data.command() @data.command(short_help="Convert dataset config to annotation set.")
@click.argument( @click.argument(
"dataset_config", "dataset_config",
type=click.Path(exists=True), type=click.Path(exists=True),
@ -70,7 +84,10 @@ def summary(
@click.option( @click.option(
"--field", "--field",
type=str, type=str,
help="If the dataset info is in a nested field please specify here.", help=(
"Nested field name that contains dataset configuration. "
"Use this when the config is wrapped in a larger file."
),
) )
@click.option( @click.option(
"--output", "--output",
@ -80,12 +97,18 @@ def summary(
@click.option( @click.option(
"--base-dir", "--base-dir",
type=click.Path(exists=True), type=click.Path(exists=True),
help="The base directory to which all recording and annotations paths are relative to.", help=(
"Base directory used to resolve relative recording and annotation "
"paths in the dataset config."
),
) )
@click.option( @click.option(
"--audio-dir", "--audio-dir",
type=click.Path(exists=True), type=click.Path(exists=True),
help="The directory containing the audio files. All paths will be relative to this directory.", help=(
"Directory containing audio files. Output annotation paths are "
"made relative to this directory."
),
) )
def convert( def convert(
dataset_config: Path, dataset_config: Path,
@ -94,7 +117,11 @@ def convert(
base_dir: Path | None = None, base_dir: Path | None = None,
audio_dir: Path | None = None, audio_dir: Path | None = None,
): ):
"""Convert a dataset config file to soundevent format.""" """Convert a dataset config into soundevent annotation-set format.
Writes a single annotation-set file that can be used by downstream tools.
Use `--audio-dir` to control relative audio path handling in the output.
"""
from soundevent import data, io from soundevent import data, io
from batdetect2.data import load_dataset, load_dataset_config from batdetect2.data import load_dataset, load_dataset_config

View File

@ -11,20 +11,73 @@ __all__ = ["evaluate_command"]
DEFAULT_OUTPUT_DIR = Path("outputs") / "evaluation" DEFAULT_OUTPUT_DIR = Path("outputs") / "evaluation"
@cli.command(name="evaluate") @cli.command(name="evaluate", short_help="Evaluate a model checkpoint.")
@click.argument("model_path", type=click.Path(exists=True)) @click.argument("model_path", type=click.Path(exists=True))
@click.argument("test_dataset", type=click.Path(exists=True)) @click.argument("test_dataset", type=click.Path(exists=True))
@click.option("--targets", "targets_config", type=click.Path(exists=True)) @click.option(
@click.option("--audio-config", type=click.Path(exists=True)) "--targets",
@click.option("--evaluation-config", type=click.Path(exists=True)) "targets_config",
@click.option("--inference-config", type=click.Path(exists=True)) type=click.Path(exists=True),
@click.option("--outputs-config", type=click.Path(exists=True)) help="Path to targets config file.",
@click.option("--logging-config", type=click.Path(exists=True)) )
@click.option("--base-dir", type=click.Path(), default=Path.cwd()) @click.option(
@click.option("--output-dir", type=click.Path(), default=DEFAULT_OUTPUT_DIR) "--audio-config",
@click.option("--experiment-name", type=str) type=click.Path(exists=True),
@click.option("--run-name", type=str) help="Path to audio config file.",
@click.option("--workers", "num_workers", type=int) )
@click.option(
"--evaluation-config",
type=click.Path(exists=True),
help="Path to evaluation config file.",
)
@click.option(
"--inference-config",
type=click.Path(exists=True),
help="Path to inference config file.",
)
@click.option(
"--outputs-config",
type=click.Path(exists=True),
help="Path to outputs config file.",
)
@click.option(
"--logging-config",
type=click.Path(exists=True),
help="Path to logging config file.",
)
@click.option(
"--base-dir",
type=click.Path(),
default=Path.cwd(),
show_default=True,
help=(
"Base directory used to resolve relative paths in the dataset "
"configuration."
),
)
@click.option(
"--output-dir",
type=click.Path(),
default=DEFAULT_OUTPUT_DIR,
show_default=True,
help="Directory where evaluation outputs are written.",
)
@click.option(
"--experiment-name",
type=str,
help="Experiment name used for logging backends.",
)
@click.option(
"--run-name",
type=str,
help="Run name used for logging backends.",
)
@click.option(
"--workers",
"num_workers",
type=int,
help="Number of worker processes for dataset loading.",
)
def evaluate_command( def evaluate_command(
model_path: Path, model_path: Path,
test_dataset: Path, test_dataset: Path,
@ -40,7 +93,11 @@ def evaluate_command(
experiment_name: str | None = None, experiment_name: str | None = None,
run_name: str | None = None, run_name: str | None = None,
): ):
"""Evaluate a checkpoint against a configured test dataset.""" """Evaluate a checkpoint against a test dataset.
Loads model and optional override configs, runs evaluation on
`test_dataset`, and writes metrics/artifacts to `output_dir`.
"""
from batdetect2.api_v2 import BatDetect2API from batdetect2.api_v2 import BatDetect2API
from batdetect2.audio import AudioConfig from batdetect2.audio import AudioConfig
from batdetect2.data import load_dataset_from_config from batdetect2.data import load_dataset_from_config

View File

@ -1,4 +1,6 @@
from functools import wraps
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING
import click import click
from loguru import logger from loguru import logger
@ -7,12 +9,89 @@ from soundevent.audio.files import get_audio_files
from batdetect2.cli.base import cli from batdetect2.cli.base import cli
if TYPE_CHECKING:
from batdetect2.api_v2 import BatDetect2API
from batdetect2.audio import AudioConfig
from batdetect2.inference import InferenceConfig
from batdetect2.outputs import OutputsConfig
__all__ = ["predict"] __all__ = ["predict"]
@cli.group(name="predict") @cli.group(name="predict", short_help="Run prediction workflows.")
def predict() -> None: def predict() -> None:
"""Run model inference on audio using API v2.""" """Run model inference on audio files.
Use one of the subcommands to select inputs from a directory, a text file
list, or an annotation dataset.
"""
def common_predict_options(func):
"""Attach options shared by all `predict` subcommands."""
@click.option(
"--audio-config",
type=click.Path(exists=True),
help=(
"Path to an audio config file. Use this to override audio "
"loading and preprocessing-related settings."
),
)
@click.option(
"--inference-config",
type=click.Path(exists=True),
help=(
"Path to an inference config file. Use this to override "
"prediction-time thresholds and behavior."
),
)
@click.option(
"--outputs-config",
type=click.Path(exists=True),
help=(
"Path to an outputs config file. Use this to control the "
"prediction fields written to disk."
),
)
@click.option(
"--logging-config",
type=click.Path(exists=True),
help=(
"Path to a logging config file. Use this to customize logging "
"format and levels."
),
)
@click.option(
"--batch-size",
type=int,
help=(
"Batch size for inference. If omitted, the value from the "
"loaded config is used."
),
)
@click.option(
"--workers",
"num_workers",
type=int,
default=0,
show_default=True,
help="Number of worker processes for audio loading.",
)
@click.option(
"--format",
"format_name",
type=str,
help=(
"Output format name used by the prediction writer. If omitted, "
"the default output format is used."
),
)
@wraps(func)
def wrapped(*args, **kwargs):
return func(*args, **kwargs)
return wrapped
def _build_api( def _build_api(
@ -21,7 +100,7 @@ def _build_api(
inference_config: Path | None, inference_config: Path | None,
outputs_config: Path | None, outputs_config: Path | None,
logging_config: Path | None, logging_config: Path | None,
): ) -> "tuple[BatDetect2API, AudioConfig | None, InferenceConfig | None, OutputsConfig | None]":
from batdetect2.api_v2 import BatDetect2API from batdetect2.api_v2 import BatDetect2API
from batdetect2.audio import AudioConfig from batdetect2.audio import AudioConfig
from batdetect2.inference import InferenceConfig from batdetect2.inference import InferenceConfig
@ -103,17 +182,14 @@ def _run_prediction(
) )
@predict.command(name="directory") @predict.command(
name="directory",
short_help="Predict on audio files in a directory.",
)
@click.argument("model_path", type=click.Path(exists=True)) @click.argument("model_path", type=click.Path(exists=True))
@click.argument("audio_dir", type=click.Path(exists=True)) @click.argument("audio_dir", type=click.Path(exists=True))
@click.argument("output_path", type=click.Path()) @click.argument("output_path", type=click.Path())
@click.option("--audio-config", type=click.Path(exists=True)) @common_predict_options
@click.option("--inference-config", type=click.Path(exists=True))
@click.option("--outputs-config", type=click.Path(exists=True))
@click.option("--logging-config", type=click.Path(exists=True))
@click.option("--batch-size", type=int)
@click.option("--workers", "num_workers", type=int, default=0)
@click.option("--format", "format_name", type=str)
def predict_directory_command( def predict_directory_command(
model_path: Path, model_path: Path,
audio_dir: Path, audio_dir: Path,
@ -126,7 +202,11 @@ def predict_directory_command(
num_workers: int, num_workers: int,
format_name: str | None, format_name: str | None,
) -> None: ) -> None:
"""Predict on all audio files in a directory.""" """Predict on all audio files in a directory.
Loads a checkpoint, scans `audio_dir` for supported audio files, runs
inference, and saves predictions to `output_path`.
"""
audio_files = list(get_audio_files(audio_dir)) audio_files = list(get_audio_files(audio_dir))
_run_prediction( _run_prediction(
model_path=model_path, model_path=model_path,
@ -142,17 +222,14 @@ def predict_directory_command(
) )
@predict.command(name="file_list") @predict.command(
name="file_list",
short_help="Predict on paths listed in a text file.",
)
@click.argument("model_path", type=click.Path(exists=True)) @click.argument("model_path", type=click.Path(exists=True))
@click.argument("file_list", type=click.Path(exists=True)) @click.argument("file_list", type=click.Path(exists=True))
@click.argument("output_path", type=click.Path()) @click.argument("output_path", type=click.Path())
@click.option("--audio-config", type=click.Path(exists=True)) @common_predict_options
@click.option("--inference-config", type=click.Path(exists=True))
@click.option("--outputs-config", type=click.Path(exists=True))
@click.option("--logging-config", type=click.Path(exists=True))
@click.option("--batch-size", type=int)
@click.option("--workers", "num_workers", type=int, default=0)
@click.option("--format", "format_name", type=str)
def predict_file_list_command( def predict_file_list_command(
model_path: Path, model_path: Path,
file_list: Path, file_list: Path,
@ -165,7 +242,11 @@ def predict_file_list_command(
num_workers: int, num_workers: int,
format_name: str | None, format_name: str | None,
) -> None: ) -> None:
"""Predict on audio files listed in a text file.""" """Predict on audio files listed in a text file.
The list file should contain one audio path per line. Empty lines are
ignored.
"""
file_list = Path(file_list) file_list = Path(file_list)
audio_files = [ audio_files = [
Path(line.strip()) Path(line.strip())
@ -187,17 +268,14 @@ def predict_file_list_command(
) )
@predict.command(name="dataset") @predict.command(
name="dataset",
short_help="Predict on recordings from a dataset config.",
)
@click.argument("model_path", type=click.Path(exists=True)) @click.argument("model_path", type=click.Path(exists=True))
@click.argument("dataset_path", type=click.Path(exists=True)) @click.argument("dataset_path", type=click.Path(exists=True))
@click.argument("output_path", type=click.Path()) @click.argument("output_path", type=click.Path())
@click.option("--audio-config", type=click.Path(exists=True)) @common_predict_options
@click.option("--inference-config", type=click.Path(exists=True))
@click.option("--outputs-config", type=click.Path(exists=True))
@click.option("--logging-config", type=click.Path(exists=True))
@click.option("--batch-size", type=int)
@click.option("--workers", "num_workers", type=int, default=0)
@click.option("--format", "format_name", type=str)
def predict_dataset_command( def predict_dataset_command(
model_path: Path, model_path: Path,
dataset_path: Path, dataset_path: Path,
@ -210,7 +288,11 @@ def predict_dataset_command(
num_workers: int, num_workers: int,
format_name: str | None, format_name: str | None,
) -> None: ) -> None:
"""Predict on recordings referenced in an annotation dataset.""" """Predict on recordings referenced in an annotation dataset.
The dataset is read as a soundevent annotation set and unique recording
paths are extracted before inference.
"""
dataset_path = Path(dataset_path) dataset_path = Path(dataset_path)
dataset = io.load(dataset_path, type="annotation_set") dataset = io.load(dataset_path, type="annotation_set")
audio_files = sorted( audio_files = sorted(

View File

@ -8,26 +8,103 @@ from batdetect2.cli.base import cli
__all__ = ["train_command"] __all__ = ["train_command"]
@cli.command(name="train") @cli.command(name="train", short_help="Train or fine-tune a model.")
@click.argument("train_dataset", type=click.Path(exists=True)) @click.argument("train_dataset", type=click.Path(exists=True))
@click.option("--val-dataset", type=click.Path(exists=True)) @click.option(
@click.option("--model", "model_path", type=click.Path(exists=True)) "--val-dataset",
@click.option("--targets", "targets_config", type=click.Path(exists=True)) type=click.Path(exists=True),
@click.option("--model-config", type=click.Path(exists=True)) help="Path to validation dataset config file.",
@click.option("--training-config", type=click.Path(exists=True)) )
@click.option("--audio-config", type=click.Path(exists=True)) @click.option(
@click.option("--evaluation-config", type=click.Path(exists=True)) "--model",
@click.option("--inference-config", type=click.Path(exists=True)) "model_path",
@click.option("--outputs-config", type=click.Path(exists=True)) type=click.Path(exists=True),
@click.option("--logging-config", type=click.Path(exists=True)) help=(
@click.option("--ckpt-dir", type=click.Path(exists=True)) "Path to a checkpoint to continue training from. If omitted, "
@click.option("--log-dir", type=click.Path(exists=True)) "training starts from a fresh model config."
@click.option("--train-workers", type=int) ),
@click.option("--val-workers", type=int) )
@click.option("--num-epochs", type=int) @click.option(
@click.option("--experiment-name", type=str) "--targets",
@click.option("--run-name", type=str) "targets_config",
@click.option("--seed", type=int) type=click.Path(exists=True),
help="Path to targets config file.",
)
@click.option(
"--model-config",
type=click.Path(exists=True),
help=("Path to model config file. Cannot be used together with --model."),
)
@click.option(
"--training-config",
type=click.Path(exists=True),
help="Path to training config file.",
)
@click.option(
"--audio-config",
type=click.Path(exists=True),
help="Path to audio config file.",
)
@click.option(
"--evaluation-config",
type=click.Path(exists=True),
help="Path to evaluation config file.",
)
@click.option(
"--inference-config",
type=click.Path(exists=True),
help="Path to inference config file.",
)
@click.option(
"--outputs-config",
type=click.Path(exists=True),
help="Path to outputs config file.",
)
@click.option(
"--logging-config",
type=click.Path(exists=True),
help="Path to logging config file.",
)
@click.option(
"--ckpt-dir",
type=click.Path(exists=True),
help="Directory where checkpoints are saved.",
)
@click.option(
"--log-dir",
type=click.Path(exists=True),
help="Directory where logs are written.",
)
@click.option(
"--train-workers",
type=int,
help="Number of worker processes for training data loading.",
)
@click.option(
"--val-workers",
type=int,
help="Number of worker processes for validation data loading.",
)
@click.option(
"--num-epochs",
type=int,
help="Maximum number of training epochs.",
)
@click.option(
"--experiment-name",
type=str,
help="Experiment name used for logging backends.",
)
@click.option(
"--run-name",
type=str,
help="Run name used for logging backends.",
)
@click.option(
"--seed",
type=int,
help="Random seed used for reproducibility.",
)
def train_command( def train_command(
train_dataset: Path, train_dataset: Path,
val_dataset: Path | None = None, val_dataset: Path | None = None,
@ -49,7 +126,12 @@ def train_command(
experiment_name: str | None = None, experiment_name: str | None = None,
run_name: str | None = None, run_name: str | None = None,
): ):
"""Train a model from dataset configs or a checkpoint.""" """Train a BatDetect2 model.
Train either from a fresh config (`--model-config`) or by fine-tuning an
existing checkpoint (`--model`). Training data are loaded from
`train_dataset`, with optional validation data from `--val-dataset`.
"""
from batdetect2.api_v2 import BatDetect2API from batdetect2.api_v2 import BatDetect2API
from batdetect2.audio import AudioConfig from batdetect2.audio import AudioConfig
from batdetect2.config import BatDetect2Config from batdetect2.config import BatDetect2Config

View File

@ -1,6 +1,10 @@
"""Main entry point for the BatDetect2 Postprocessing pipeline.""" """Main entry point for the BatDetect2 Postprocessing pipeline."""
from batdetect2.postprocess.config import PostprocessConfig from batdetect2.postprocess.config import (
DEFAULT_CLASSIFICATION_THRESHOLD,
DEFAULT_DETECTION_THRESHOLD,
PostprocessConfig,
)
from batdetect2.postprocess.nms import non_max_suppression from batdetect2.postprocess.nms import non_max_suppression
from batdetect2.postprocess.postprocessor import ( from batdetect2.postprocess.postprocessor import (
Postprocessor, Postprocessor,
@ -28,4 +32,6 @@ __all__ = [
"PostprocessorProtocol", "PostprocessorProtocol",
"build_postprocessor", "build_postprocessor",
"non_max_suppression", "non_max_suppression",
"DEFAULT_CLASSIFICATION_THRESHOLD",
"DEFAULT_DETECTION_THRESHOLD",
] ]

View File

@ -1,23 +1,19 @@
"""Handles mapping between geometric ROIs and target representations. """Map geometric ROIs to target representations and back.
This module defines a standardized interface (`ROITargetMapper`) for converting This module defines a standardized interface (`ROITargetMapper`) for converting
a sound event's Region of Interest (ROI) into a target representation suitable a sound event ROI into a target representation for model training and decoding
for machine learning models, and for decoding model outputs back into geometric model outputs back into approximate geometries.
ROIs.
The core operations are: Core operations:
1. **Encoding**: A `soundevent.data.SoundEvent` is mapped to a reference
`Position` (time, frequency) and a `Size` array. The method for
determining the position and size varies by the mapper implementation
(e.g., using a bounding box anchor or the point of peak energy).
2. **Decoding**: A `Position` and `Size` array are mapped back to an
approximate `soundevent.data.Geometry` (typically a `BoundingBox`).
This logic is encapsulated within specific mapper classes. Configuration for - Encode a `soundevent.data.SoundEvent` into a reference `Position`
each mapper (e.g., anchor point, scaling factors) is managed by a corresponding `(time, frequency)` and a `Size` array.
Pydantic config object. The `ROIMapperConfig` type allows for flexibly - Decode a `Position` and `Size` array into an approximate
selecting and configuring the desired mapper. This module separates the `soundevent.data.Geometry` (usually a `BoundingBox`).
*geometric* aspect of target definition from *semantic* classification.
The specific mapping depends on the selected mapper implementation. Config
objects provide mapper-specific parameters such as anchor choice and scaling.
This module focuses on the geometric part of target definition.
""" """
from typing import Annotated, Literal from typing import Annotated, Literal
@ -131,12 +127,12 @@ class AnchorBBoxMapper(ROITargetMapper):
This class implements the `ROITargetMapper` protocol for `BoundingBox` This class implements the `ROITargetMapper` protocol for `BoundingBox`
geometries. geometries.
**Encoding**: The `position` is a fixed anchor point on the bounding box Encoding uses a fixed anchor point on the bounding box for `position`
(e.g., "bottom-left"). The `size` is a 2-element array containing the (for example, ``bottom-left``). The `size` is a 2-element array with
scaled width and height of the box. scaled width and height.
**Decoding**: Reconstructs a `BoundingBox` from an anchor point and Decoding reconstructs a `BoundingBox` from anchor position and scaled
scaled width/height. width/height.
Attributes Attributes
---------- ----------
@ -300,13 +296,12 @@ class PeakEnergyBBoxMapper(ROITargetMapper):
This class implements the `ROITargetMapper` protocol. This class implements the `ROITargetMapper` protocol.
**Encoding**: The `position` is the (time, frequency) coordinate of the Encoding sets `position` to the (time, frequency) coordinate of peak energy
point with the highest energy within the sound event's bounding box. The inside the sound event bounding box. The `size` is a 4-element array with
`size` is a 4-element array representing the scaled distances from this scaled distances from the peak point to left, bottom, right, and top edges.
peak energy point to the left, bottom, right, and top edges of the box.
**Decoding**: Reconstructs a `BoundingBox` by adding/subtracting the Decoding reconstructs a `BoundingBox` by applying the unscaled distances to
un-scaled distances from the peak energy point. the peak-energy position.
Attributes Attributes
---------- ----------