Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bats_ai/core/admin/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class ConfigurationAdmin(admin.ModelAdmin):
"display_pulse_annotations",
"display_sequence_annotations",
"run_inference_on_upload",
"create_pulse_annotations_from_batbot",
"spectrogram_x_stretch",
"spectrogram_view",
]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 4.2.23 on 2026-06-11 00:00
from __future__ import annotations

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("core", "0040_alter_grtscells_id"),
]

operations = [
migrations.AddField(
model_name="configuration",
name="create_pulse_annotations_from_batbot",
field=models.BooleanField(default=False),
),
]
1 change: 1 addition & 0 deletions bats_ai/core/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class AvailableColorScheme(models.TextChoices):
display_pulse_annotations = models.BooleanField(default=True)
display_sequence_annotations = models.BooleanField(default=True)
run_inference_on_upload = models.BooleanField(default=True)
create_pulse_annotations_from_batbot = models.BooleanField(default=False)
spectrogram_x_stretch = models.DecimalField(default=2.5, max_digits=3, decimal_places=2)
spectrogram_view = models.CharField(
max_length=12, choices=SpectrogramViewMode, default=SpectrogramViewMode.COMPRESSED
Expand Down
9 changes: 9 additions & 0 deletions bats_ai/core/tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@ def recording_compute_spectrogram(self, recording_id: int): # noqa: C901, PLR09
pulse_metadata_obj.contours = []
pulse_metadata_obj.save()

from bats_ai.core.utils.batbot_annotations import (
create_pulse_annotations_from_batbot_segments,
)

create_pulse_annotations_from_batbot_segments(
recording,
compressed["segments"],
)

if processing_task:
processing_task.status = ProcessingTask.Status.COMPLETE
processing_task.save()
Expand Down
69 changes: 69 additions & 0 deletions bats_ai/core/utils/batbot_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from bats_ai.core.models import Annotations, Configuration

if TYPE_CHECKING:
from bats_ai.core.models import Recording
from bats_ai.core.utils.batbot_metadata import BatBotMetadataCurve

logger = logging.getLogger(__name__)

BATBOT_ANNOTATION_MODEL = "batbot"


def _segment_bounds(
segment: BatBotMetadataCurve,
) -> tuple[float, float, float, float] | None:
curve = segment.get("curve_hz_ms") or []
if not curve:
return None

times = [pt[1] for pt in curve]
freqs = [pt[0] for pt in curve]
return min(times), max(times), min(freqs), max(freqs)


def create_pulse_annotations_from_batbot_segments(
recording: Recording,
segments: list[BatBotMetadataCurve],
) -> int:
"""Create pulse annotations from BatBot segments when enabled in Configuration."""
config = Configuration.objects.first()
if not config or not config.create_pulse_annotations_from_batbot:
return 0

Annotations.objects.filter(
recording=recording,
model=BATBOT_ANNOTATION_MODEL,
).delete()

created = 0
for segment in segments:
bounds = _segment_bounds(segment)
if bounds is None:
segment_index = segment.get("segment_index")
logger.warning(
"Skipping BatBot pulse annotation for recording=%s segment_index=%s: no bbox",
recording.pk,
segment_index,
)
continue

t_start, t_end, f_lo, f_hi = bounds
Annotations.objects.create(
recording=recording,
owner=recording.owner,
start_time=t_start,
end_time=t_end,
low_freq=f_lo,
high_freq=f_hi,
type="pulse",
model=BATBOT_ANNOTATION_MODEL,
comments="",
)
created += 1

return created
4 changes: 3 additions & 1 deletion bats_ai/core/views/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class ConfigurationSchema(Schema):
display_sequence_annotations: bool
is_admin: bool | None = None
run_inference_on_upload: bool
create_pulse_annotations_from_batbot: bool
spectrogram_x_stretch: float
spectrogram_view: Configuration.SpectrogramViewMode
default_color_scheme: Configuration.AvailableColorScheme
Expand All @@ -44,6 +45,7 @@ def get_configuration(request):
display_pulse_annotations=config.display_pulse_annotations,
display_sequence_annotations=config.display_sequence_annotations,
run_inference_on_upload=config.run_inference_on_upload,
create_pulse_annotations_from_batbot=config.create_pulse_annotations_from_batbot,
spectrogram_x_stretch=config.spectrogram_x_stretch,
spectrogram_view=config.spectrogram_view,
default_color_scheme=config.default_color_scheme,
Expand All @@ -62,7 +64,7 @@ def update_configuration(request, payload: ConfigurationSchema):
config = Configuration.objects.first()
if not config:
return JsonResponse({"error": "No configuration found"}, status=404)
for attr, value in payload.dict().items():
for attr, value in payload.dict(exclude={"is_admin"}).items():
setattr(config, attr, value)
config.save()
return ConfigurationSchema.from_orm(config)
Expand Down
32 changes: 16 additions & 16 deletions bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,12 +178,12 @@ class RecordingPaginatedResponse(Schema):


class AnnotationSchema(Schema):
start_time: int
end_time: int
low_freq: int
high_freq: int
start_time: float
end_time: float
low_freq: float
high_freq: float
species: list[SpeciesSchema]
comments: str
comments: str = ""
type: str | None = None
id: int | None = None
owner_email: str = None
Expand All @@ -196,18 +196,18 @@ def from_orm(cls, obj: Annotations, owner_email=None):
low_freq=obj.low_freq,
high_freq=obj.high_freq,
species=[SpeciesSchema.from_orm(species) for species in obj.species.all()],
comments=obj.comments,
comments=obj.comments or "",
id=obj.id,
type=obj.type,
owner_email=owner_email, # Include owner_email in the schema
)


class UpdateAnnotationsSchema(Schema):
start_time: int | None
end_time: int | None
low_freq: int | None
high_freq: int | None
start_time: float | None
end_time: float | None
low_freq: float | None
high_freq: float | None
species: list[SpeciesSchema] | None
comments: str | None
type: str | None
Expand Down Expand Up @@ -278,10 +278,10 @@ def linestring_to_list(ls):

class SequenceAnnotationSchema(Schema):
id: int
start_time: int
end_time: int
start_time: float
end_time: float
type: str | None
comments: str
comments: str = ""
species: list[SpeciesSchema] | None
owner_email: str = None

Expand All @@ -292,15 +292,15 @@ def from_orm(cls, obj, owner_email=None):
end_time=obj.end_time,
type=obj.type,
species=[SpeciesSchema.from_orm(species) for species in obj.species.all()],
comments=obj.comments,
comments=obj.comments or "",
id=obj.id,
owner_email=owner_email, # Include owner_email in the schema
)


class UpdateSequenceAnnotationSchema(Schema):
start_time: int = None
end_time: int = None
start_time: float = None
end_time: float = None
type: str | None = None
comments: str | None = None

Expand Down
1 change: 1 addition & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,7 @@ export interface ConfigurationSettings {
display_pulse_annotations: boolean;
display_sequence_annotations: boolean;
run_inference_on_upload: boolean;
create_pulse_annotations_from_batbot: boolean;
spectrogram_x_stretch: number;
spectrogram_view: "compressed" | "uncompressed";
is_admin?: boolean;
Expand Down
8 changes: 7 additions & 1 deletion client/src/components/AnnotationList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
SpectrogramSequenceAnnotation,
} from "../api/api";
import RecordingAnnotations from "./RecordingAnnotations.vue";
import { formatSignificantDigits } from "@use/useUtils";
export default defineComponent({
name: "AnnotationList",
components: {
Expand Down Expand Up @@ -107,6 +108,7 @@ export default defineComponent({
annotationState,
annotations,
creationType,
formatSignificantDigits,
sequenceAnnotations,
selectedId,
selectedType,
Expand Down Expand Up @@ -191,7 +193,11 @@ export default defineComponent({
>
<span class="pl-2"
><b
>({{ annotation.end_time - annotation.start_time }}ms)</b
>({{
formatSignificantDigits(
annotation.end_time - annotation.start_time,
)
}}ms)</b
></span
>
</v-col>
Expand Down
5 changes: 3 additions & 2 deletions client/src/components/PulseMetadataTooltip.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<script lang="ts">
import { defineComponent, nextTick, type PropType, ref, watch } from "vue";
import type { PulseMetadataTooltipData } from "./geoJS/layers/pulseMetadataLayer";
import { formatSignificantDigits } from "@use/useUtils";

const MIN_WIDTH = 180;

Expand Down Expand Up @@ -41,7 +42,7 @@ export default defineComponent({
{ immediate: true },
);

return { cardRef, clampedLeft };
return { cardRef, clampedLeft, formatSignificantDigits };
},
});
</script>
Expand Down Expand Up @@ -113,7 +114,7 @@ export default defineComponent({
</div> -->
<div class="d-flex align-center">
<span class="text-caption text-medium-emphasis mr-2">Duration</span>
<span>{{ data.durationMs.toFixed(1) }} ms</span>
<span>{{ formatSignificantDigits(data.durationMs) }} ms</span>
</div>
<div class="d-flex align-center">
<span class="text-caption text-medium-emphasis mr-2">Fₘᵢₙ</span>
Expand Down
8 changes: 5 additions & 3 deletions client/src/components/SpectrogramViewer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,12 @@ export default defineComponent({
found,
props.spectroInfo,
selectedType.value,
scaledWidth.value,
scaledHeight.value,
);
const bounds = geoJS.getGeoViewer().value.bounds();
if (x < bounds.left || x > bounds.right) {
geoJS.getGeoViewer().value.center({ x, y });
const viewer = geoJS.getGeoViewer().value;
if (viewer && x >= 0 && y >= 0) {
viewer.center({ x, y });
}
}
});
Expand Down
22 changes: 17 additions & 5 deletions client/src/components/geoJS/geoJSUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,18 +914,30 @@ function spectroToCenter(
annotation: SpectrogramAnnotation | SpectrogramSequenceAnnotation,
spectroInfo: SpectroInfo,
type: "sequence" | "pulse",
scaledWidth = 0,
scaledHeight = 0,
) {
if (type === "pulse") {
const geoJSON = spectroToGeoJSon(
annotation as SpectrogramAnnotation,
spectroInfo,
);
return findPolygonCenter(geoJSON);
const pulse = annotation as SpectrogramAnnotation;
const centerTime = (pulse.start_time + pulse.end_time) / 2;
const x = spectroTimeToX(centerTime, spectroInfo, scaledWidth);
const adjustedHeight =
scaledHeight > spectroInfo.height ? scaledHeight : spectroInfo.height;
const centerFreq = (pulse.low_freq + pulse.high_freq) / 2;
const heightScale =
adjustedHeight / (spectroInfo.high_freq - spectroInfo.low_freq);
const y =
adjustedHeight - (centerFreq - spectroInfo.low_freq) * heightScale;
return [x, y];
}
if (type === "sequence") {
const geoJSON = spectroSequenceToGeoJSon(
annotation as SpectrogramSequenceAnnotation,
spectroInfo,
0,
10,
scaledWidth,
scaledHeight,
);
return findPolygonCenter(geoJSON);
}
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/geoJS/layers/pulseMetadataLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { PulseMetadata } from "@api/api";
import type { LayerStyle, LineData, TextData } from "./types";
import BaseTextLayer from "./baseTextLayer";
import type { PulseMetadataLabelsMode } from "@use/usePulseMetadata";
import { formatSignificantDigits } from "@use/useUtils";

/** Point data for char_freq, knee, heel with pixel coords and label. */
interface PulsePointData {
Expand Down Expand Up @@ -398,7 +399,7 @@ export default class PulseMetadataLayer extends BaseTextLayer<TextData> {

const durationMidX = (bottomLeft.x + bottomRight.x) / 2;
pulseText.push({
text: `${durationMs.toFixed(1)} ms`,
text: `${formatSignificantDigits(durationMs)} ms`,
x: durationMidX,
y: bottomLeft.y + labelOffset,
offsetX: 0,
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/geoJS/layers/timeLayer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
spectroSequenceToGeoJSon,
spectroToGeoJSon,
} from "../geoJSUtils";
import { formatSignificantDigits } from "@use/useUtils";
import BaseTextLayer from "./baseTextLayer";
import type { LayerStyle } from "./types";

Expand Down Expand Up @@ -242,7 +243,7 @@ export default class TimeLayer extends BaseTextLayer<TextData> {
const ypos = (ymax + ymin) / 2.0;
// Now we need to create the text Labels
this.textData.push({
text: `${end_time - start_time}ₘₛ`,
text: `${formatSignificantDigits(end_time - start_time)}ₘₛ`,
x: xpos,
y: ypos + lineDist,
});
Expand Down
1 change: 1 addition & 0 deletions client/src/use/useState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const configuration: Ref<Configuration> = ref({
spectrogram_view: "compressed",
spectrogram_x_stretch: 2.5,
run_inference_on_upload: true,
create_pulse_annotations_from_batbot: false,
default_color_scheme: "inferno",
default_spectrogram_background_color: "rgb(0, 0, 0)",
is_admin: false,
Expand Down
8 changes: 8 additions & 0 deletions client/src/use/useUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ function extractDateTimeComponents(dateTimeString: string) {
return { date: dateString, time: timeString };
}

/** Format a numeric value to the given number of significant digits. */
function formatSignificantDigits(value: number, significantDigits = 3): string {
if (!Number.isFinite(value)) return String(value);
if (value === 0) return "0";
return String(Number(value.toPrecision(significantDigits)));
}

function getImageDimensions(
images: HTMLImageElement[],
fallback: { width: number; height: number } = { width: 0, height: 0 },
Expand Down Expand Up @@ -96,6 +103,7 @@ function parseRecordingFilename(

export {
DEFAULT_SAMPLE_FRAME_ID,
formatSignificantDigits,
getCurrentTime,
extractDateTimeComponents,
getImageDimensions,
Expand Down
Loading