Skip to content
Merged
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
26 changes: 25 additions & 1 deletion bats_ai/core/admin/nabat/admin.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations

from django import forms
from django.contrib import admin
from django.contrib.gis.db import models as gis_models
from django.utils.html import format_html_join

from bats_ai.core.models.nabat import (
Expand All @@ -9,6 +11,7 @@
NABatRecordingAnnotation,
NABatSpectrogram,
)
from bats_ai.core.models.nabat.nabat_pulse_metadata import NABatPulseMetadata


# Register models for the NaBat category
Expand Down Expand Up @@ -88,7 +91,28 @@ class NABatRecordingAdmin(admin.ModelAdmin):
"comments",
"recording_location",
"grts_cell_id",
"grts_cell",
"sample_frame_id",
]
search_fields = ["name", "recording_id", "recording_location"]
list_filter = ["name", "recording_id", "recording_location"]


@admin.register(NABatPulseMetadata)
class NABatPulseMetadataAdmin(admin.ModelAdmin):
formfield_overrides = {
gis_models.GeometryField: {
"widget": forms.Textarea(attrs={"rows": 4, "cols": 80}),
},
}

list_display = [
"nabat_recording",
"index",
"bounding_box",
"curve",
"char_freq",
"knee",
"heel",
"slopes",
]
list_select_related = True
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Generated by Django 6.0.7 on 2026-08-05 20:08

from __future__ import annotations

import django.contrib.gis.db.models.fields
from django.db import migrations, models
import django.db.models.deletion


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

operations = [
migrations.RenameField(
model_name="nabatrecording",
old_name="grts_cell",
new_name="sample_frame_id",
),
migrations.CreateModel(
name="NABatPulseMetadata",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
("index", models.IntegerField()),
("bounding_box", django.contrib.gis.db.models.fields.PolygonField(srid=4326)),
("contours", models.JSONField(blank=True, null=True)),
(
"curve",
django.contrib.gis.db.models.fields.LineStringField(
blank=True, null=True, srid=4326
),
),
(
"char_freq",
django.contrib.gis.db.models.fields.PointField(
blank=True, null=True, srid=4326
),
),
(
"knee",
django.contrib.gis.db.models.fields.PointField(
blank=True, null=True, srid=4326
),
),
(
"heel",
django.contrib.gis.db.models.fields.PointField(
blank=True, null=True, srid=4326
),
),
("slopes", models.JSONField(blank=True, null=True)),
(
"nabat_recording",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="core.nabatrecording"
),
),
],
options={
"verbose_name": "NABat Pulse Metadata",
"verbose_name_plural": "NABat Pulse Metadata",
},
),
]
2 changes: 2 additions & 0 deletions bats_ai/core/models/nabat/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import annotations

from .nabat_compressed_spectrogram import NABatCompressedSpectrogram
from .nabat_pulse_metadata import NABatPulseMetadata
from .nabat_recording import NABatRecording
from .nabat_recording_annotation import NABatRecordingAnnotation
from .nabat_spectrogram import NABatSpectrogram

__all__ = [
"NABatCompressedSpectrogram",
"NABatPulseMetadata",
"NABatRecording",
"NABatRecordingAnnotation",
"NABatSpectrogram",
Expand Down
21 changes: 21 additions & 0 deletions bats_ai/core/models/nabat/nabat_pulse_metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from __future__ import annotations

from django.contrib.gis.db import models

from .nabat_recording import NABatRecording


class NABatPulseMetadata(models.Model):
nabat_recording = models.ForeignKey(NABatRecording, on_delete=models.CASCADE)
index = models.IntegerField(null=False, blank=False)
bounding_box = models.PolygonField(null=False, blank=False)
contours = models.JSONField(null=True, blank=True)
curve = models.LineStringField(null=True, blank=True)
char_freq = models.PointField(null=True, blank=True)
knee = models.PointField(null=True, blank=True)
heel = models.PointField(null=True, blank=True)
slopes = models.JSONField(null=True, blank=True)

class Meta:
verbose_name = "NABat Pulse Metadata"
verbose_name_plural = "NABat Pulse Metadata"
2 changes: 1 addition & 1 deletion bats_ai/core/models/nabat/nabat_recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class NABatRecording(TimeStampedModel, models.Model):
comments = models.TextField(blank=True, null=True)
recording_location = models.GeometryField(srid=4326, blank=True, null=True)
grts_cell_id = models.IntegerField(blank=True, null=True)
grts_cell = models.IntegerField(blank=True, null=True)
sample_frame_id = models.IntegerField(blank=True, null=True)
public = models.BooleanField(default=False)
software_name = models.TextField(blank=True, null=True)
software_developer = models.TextField(blank=True, null=True)
Expand Down
13 changes: 7 additions & 6 deletions bats_ai/core/tasks/nabat/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from django.contrib.gis.geos import LineString, Point, Polygon
import requests

from bats_ai.core.models import ProcessingTask, PulseMetadata
from bats_ai.core.models import ProcessingTask
from bats_ai.core.models.nabat import NABatPulseMetadata
from bats_ai.utils.spectrogram_utils import (
generate_nabat_compressed_spectrogram,
generate_nabat_spectrogram,
Expand Down Expand Up @@ -66,8 +67,8 @@ def generate_spectrograms( # noqa: C901, PLR0915
segment_index_map = {}
contour_segments = compressed.get("contours", {}).get("segments", [])
for segment in contour_segments:
pulse_metadata_obj, _ = PulseMetadata.objects.get_or_create(
recording=compressed_obj.recording,
pulse_metadata_obj, _ = NABatPulseMetadata.objects.get_or_create(
nabat_recording=compressed_obj.nabat_recording,
index=segment["segment_index"],
defaults={
"contours": segment["contours"],
Expand All @@ -92,7 +93,7 @@ def generate_spectrograms( # noqa: C901, PLR0915
"heel": Point(segment["heel_ms"], segment["heel_hz"]),
"slopes": segment.get("slopes"),
}
# `PulseMetadata.bounding_box` is non-nullable, so always populate it
# `NABatPulseMetadata.bounding_box` is non-nullable, so always populate it
# for rows not created from `compressed["contours"]`.
segment_bbox = segment.get("bbox")
if segment_bbox and len(segment_bbox) == 4:
Expand Down Expand Up @@ -121,8 +122,8 @@ def generate_spectrograms( # noqa: C901, PLR0915
if not settings.BATAI_SAVE_SPECTROGRAM_CONTOURS:
defaults["contours"] = []

PulseMetadata.objects.update_or_create(
recording=compressed_obj.recording,
NABatPulseMetadata.objects.update_or_create(
nabat_recording=compressed_obj.nabat_recording,
index=segment["segment_index"],
defaults=defaults,
)
Expand Down
43 changes: 23 additions & 20 deletions bats_ai/core/views/species.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,13 @@

from typing import TYPE_CHECKING

from django.db.models import (
BooleanField,
Case,
Exists,
OuterRef,
Value,
When,
)
from django.db.models import BooleanField, Case, Exists, OuterRef, Value, When
from django.shortcuts import get_object_or_404
from ninja import Query, Router, Schema

from bats_ai.core.constants import DEFAULT_SAMPLE_FRAME_ID
from bats_ai.core.models import GRTSCells, Recording, Species, SpeciesRange
from bats_ai.core.models.nabat.nabat_recording import NABatRecording
from bats_ai.core.utils.grts_utils import normalize_sample_frame_id

if TYPE_CHECKING:
Expand All @@ -40,20 +34,29 @@ class SpeciesSchema(Schema):
in_range: bool | None = None


class SpeciesQuerySchema(Schema):
grts_cell_id: int | None = None
sample_frame_id: int = DEFAULT_SAMPLE_FRAME_ID
recording_id: int | None = None
nabat: bool | None = None


@router.get("/", response=list[SpeciesSchema], auth=None)
def get_species(
request: HttpRequest,
grts_cell_id: int | None = Query(None),
sample_frame_id: int = Query(DEFAULT_SAMPLE_FRAME_ID),
recording_id: int | None = Query(None),
):
sample_frame_id = normalize_sample_frame_id(sample_frame_id)
def get_species(request: HttpRequest, q: Query[SpeciesQuerySchema]):
grts_cell_id = q.grts_cell_id
sample_frame_id = normalize_sample_frame_id(q.sample_frame_id)

if recording_id is not None:
recording = get_object_or_404(
Recording.objects.only("grts_cell_id", "sample_frame_id"),
pk=recording_id,
)
if q.recording_id is not None:
recording = None
if not q.nabat:
recording = get_object_or_404(
Recording.objects.only("grts_cell_id", "sample_frame_id"),
pk=q.recording_id,
)
else:
recording = get_object_or_404(
NABatRecording.objects.only("grts_cell_id", "sample_frame_id"), pk=q.recording_id
)
grts_cell_id = recording.grts_cell_id
sample_frame_id = normalize_sample_frame_id(
recording.sample_frame_id
Expand Down
21 changes: 21 additions & 0 deletions client/src/api/NABatApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type ProcessingTask,
type Spectrogram,
type UpdateFileAnnotation,
type Species,
} from "./api";

export interface NABatRecordingCompleteResponse {
Expand Down Expand Up @@ -85,6 +86,25 @@ async function getNABatFileAnnotations(recordingId: number) {
);
}

async function getNABatSpecies({
recordingId,
grtsCellId,
sampleFrameId,
}: {
recordingId?: number;
grtsCellId?: number;
sampleFrameId?: number;
}) {
return axiosInstance.get<Species[]>("/species/", {
params: {
recording_id: recordingId,
grts_cell_id: grtsCellId,
sample_frame_id: sampleFrameId,
nabat: true,
},
});
}

async function getNABatFileAnnotationDetails(
recordingId: number,
apiToken?: string,
Expand Down Expand Up @@ -261,6 +281,7 @@ export {
getNABatSpectrogramCompressed,
getNABatRecordingFileAnnotations,
getNABatFileAnnotations,
getNABatSpecies,
getNABatFileAnnotationDetails,
putNABatFileAnnotation,
patchNABatFileAnnotationLocal,
Expand Down
5 changes: 3 additions & 2 deletions client/src/views/NABat/NABatSpectrogram.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<script lang="ts">
import { defineComponent, onMounted, type Ref, ref, watch } from "vue";
import { getSpecies, type Species } from "@api/api";
import { type Species } from "@api/api";
import {
getNABatSpectrogram,
getNABatSpectrogramCompressed,
getNABatSpecies,
} from "@api/NABatApi";
import SpectrogramViewer from "@components/SpectrogramViewer.vue";
import { spectroXToTime, type SpectroInfo } from "@components/geoJS/geoJSUtils";
Expand Down Expand Up @@ -119,7 +120,7 @@ export default defineComponent({
spectroInfo.value.end_times = response.data.compressed.end_times;
viewCompressedOverlay.value = false;
}
const speciesResponse = await getSpecies({
const speciesResponse = await getNABatSpecies({
recordingId: parseInt(props.id),
});
// Removing NOISE species from list and any duplicates
Expand Down