Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
f1c3782
initial testing and integration
BryonLewis Jan 16, 2026
db3834a
batbot metadata parser
BryonLewis Jan 16, 2026
4253365
Merge branch 'main' into batbot-integration
BryonLewis Jan 19, 2026
5c3d9a8
batbot spectrogram generation
BryonLewis Jan 22, 2026
1c24339
remove old spectrogram generation code
BryonLewis Jan 22, 2026
8878620
swap back to using the github installation for batbot
BryonLewis Jan 26, 2026
e96da72
Merge branch 'batbot-integration' of https://github.com/Kitware/batai…
BryonLewis Jan 26, 2026
9623991
use temp branch for start/stop fixes
BryonLewis Jan 27, 2026
a1d5eed
increase accuracy for spectrograms and annotations
BryonLewis Jan 27, 2026
a438016
thumbnail centering fixes
BryonLewis Jan 27, 2026
b1ae312
add noise filter
BryonLewis Jan 28, 2026
4aa3016
contour testing
BryonLewis Jan 28, 2026
6cc0530
contours backend
BryonLewis Jan 29, 2026
8f1ff79
contour support
BryonLewis Jan 29, 2026
02d6d27
update batbot
BryonLewis Jan 29, 2026
9fcea27
Merge branch 'batbot-integration' into batbot-contours
BryonLewis Jan 29, 2026
028843b
fix contour width calculations
BryonLewis Jan 29, 2026
968a433
contour opacity settings
BryonLewis Jan 29, 2026
a5ebef3
contour testing
BryonLewis Jan 30, 2026
439dae3
Merge branch 'main' into batbot-integration
BryonLewis Jan 30, 2026
d95e58b
fix NABat spectrogram generation
BryonLewis Jan 30, 2026
0bed12a
client linting
BryonLewis Jan 30, 2026
dacd997
removing integration notes
BryonLewis Jan 30, 2026
58f29d1
Merge branch 'batbot-integration' into batbot-contours
BryonLewis Jan 30, 2026
52f9ba6
use masks for contours
BryonLewis Jan 30, 2026
6a6fa8f
save mask images along compressed spectrograms
BryonLewis Jan 30, 2026
6b43dfe
contour and mask UI
BryonLewis Jan 30, 2026
5ffb23a
batbot metadata graphing
BryonLewis Jan 30, 2026
1ad4670
reconfigure pulseMetadata, add labels
BryonLewis Feb 2, 2026
cc1c694
Update client/src/components/TransparencyFilterControl.vue
BryonLewis Feb 3, 2026
9747b84
reverting float to ints for pixel fields
BryonLewis Feb 3, 2026
78c1093
remove uneeded dependencies
BryonLewis Feb 3, 2026
bf1fa9c
Merge branch 'main' into batbot-integration
BryonLewis Feb 3, 2026
856081b
import GRTS updated for sciencebase.gov downtime
BryonLewis Feb 3, 2026
29b0d5d
add batbot issue for ml model integration
BryonLewis Feb 3, 2026
ba11371
main merge migration update
BryonLewis Feb 3, 2026
b18710b
swap to port 8080 for client based on main's client redirect port
BryonLewis Feb 3, 2026
b50e74d
Merge branch 'batbot-integration' into batbot-contours
BryonLewis Feb 3, 2026
25d4475
remaking migrations
BryonLewis Feb 3, 2026
53a7250
Merge branch 'batbot-contours' into batbot-metadata-drawing
BryonLewis Feb 3, 2026
7867276
migrations
BryonLewis Feb 3, 2026
f3bbc7d
Merge branch 'main' into batbot-contours
BryonLewis Feb 4, 2026
b7abb14
remove svgwrite depedency
BryonLewis Feb 4, 2026
21ee18f
rename extract_contours script
BryonLewis Feb 4, 2026
3bc9f65
client side fixes to contour toggling
BryonLewis Feb 4, 2026
1531d4c
update pulse metadata if the compute spectrogram is run again
BryonLewis Feb 4, 2026
c7b4c10
remove vetting details print
BryonLewis Feb 4, 2026
9852f08
Merge branch 'batbot-contours' into batbot-metadata-drawing
BryonLewis Feb 4, 2026
d756c9b
remove duplicate characteristic frequency plotting
BryonLewis Feb 4, 2026
10a85b8
make contours optional for pulseMetadata
BryonLewis Feb 4, 2026
49f5cad
update migrations
BryonLewis Feb 4, 2026
bf1d681
Merge branch 'batbot-contours' into batbot-metadata-drawing
BryonLewis Feb 4, 2026
53e9012
fix v-if, fix type for key points, nabat update or create
BryonLewis Feb 4, 2026
b7acb5c
swap to batbot main branch
BryonLewis Feb 5, 2026
fb305b0
Merge branch 'main' into batbot-contours
BryonLewis Feb 5, 2026
2ad857f
Merge branch 'batbot-contours' into batbot-metadata-drawing
BryonLewis Feb 5, 2026
4f4f313
Update client/src/use/useState.ts
BryonLewis Feb 5, 2026
e49c63b
Update client/src/views/Spectrogram.vue
BryonLewis Feb 5, 2026
12c2e94
addressing comments
BryonLewis Feb 6, 2026
fde1f1c
Merge branch 'main' into batbot-contours
BryonLewis Feb 6, 2026
effe76c
contour layer ordering
BryonLewis Feb 6, 2026
42529ca
Merge branch 'batbot-contours' into batbot-metadata-drawing
BryonLewis Feb 6, 2026
2f6ed55
Merge branch 'main' into batbot-metadata-drawing
BryonLewis Feb 6, 2026
9478be5
move pulse metadata to it's own usePulseMetadata singleton composable
BryonLewis Feb 6, 2026
ae4fbc9
Merge branch 'main' into batbot-metadata-drawing
BryonLewis Feb 10, 2026
1d864d0
remove deep watching from the pulseMetadata settings
BryonLewis Feb 10, 2026
d6ace10
swap to baseTextLayer base for pulsemetadata and use textScaling for …
BryonLewis Feb 10, 2026
0c41342
hover to open contours
BryonLewis Feb 10, 2026
521077e
positioning of metadata text labels
BryonLewis Feb 10, 2026
b628178
swap to pypi for batbot installation
BryonLewis Feb 10, 2026
d9a823d
Merge branch 'main' into batbot-metadata-drawing
BryonLewis Feb 11, 2026
059a451
installing tasks for celery container
BryonLewis Feb 11, 2026
82e6698
revert command structure and use uv run --extra
BryonLewis Feb 11, 2026
9723474
modify the celery command and cache volume name
BryonLewis Feb 11, 2026
05b46ed
share cache among the containers
BryonLewis Feb 11, 2026
d888c23
Merge branch 'main' into batbot-metadata-drawing
BryonLewis Feb 12, 2026
779ce2a
Merge branch 'main' into batbot-metadata-drawing
BryonLewis Feb 12, 2026
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
2 changes: 1 addition & 1 deletion bats_ai/core/admin/pulse_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@

@admin.register(PulseMetadata)
class PulseMetadataAdmin(admin.ModelAdmin):
list_display = ('recording', 'index', 'bounding_box')
list_display = ('recording', 'index', 'bounding_box', 'curve', 'char_freq', 'knee', 'heel')
list_select_related = True
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Generated by Django 4.2.23 on 2026-02-03 19:43

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


class Migration(migrations.Migration):
dependencies = [
('core', '0028_alter_spectrogramimage_type_pulsemetadata'),
]

operations = [
migrations.AddField(
model_name='pulsemetadata',
name='char_freq',
field=django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326),
),
migrations.AddField(
model_name='pulsemetadata',
name='curve',
field=django.contrib.gis.db.models.fields.LineStringField(
blank=True, null=True, srid=4326
),
),
migrations.AddField(
model_name='pulsemetadata',
name='heel',
field=django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326),
),
migrations.AddField(
model_name='pulsemetadata',
name='knee',
field=django.contrib.gis.db.models.fields.PointField(blank=True, null=True, srid=4326),
),
]
5 changes: 4 additions & 1 deletion bats_ai/core/models/pulse_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,7 @@ class PulseMetadata(models.Model):
index = models.IntegerField(null=False, blank=False)
bounding_box = models.PolygonField(null=False, blank=False)
contours = models.JSONField(null=True, blank=True)
# TODO: Add in metadata from batbot
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)
49 changes: 47 additions & 2 deletions bats_ai/core/tasks/nabat/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
from pathlib import Path
import tempfile

from django.contrib.gis.geos import LineString, Point, Polygon
import requests

from bats_ai.core.models import ProcessingTask
from bats_ai.core.models import ProcessingTask, PulseMetadata
from bats_ai.core.models.nabat import NABatRecording
from bats_ai.utils.spectrogram_utils import (
generate_nabat_compressed_spectrogram,
Expand Down Expand Up @@ -54,7 +55,51 @@ def generate_spectrograms(

compressed = results['compressed']

generate_nabat_compressed_spectrogram(nabat_recording, spectrogram, compressed)
compressed_obj = generate_nabat_compressed_spectrogram(
nabat_recording, spectrogram, compressed
)
segment_index_map = {}
for segment in compressed['contours']['segments']:
pulse_metadata_obj, _ = PulseMetadata.objects.get_or_create(
recording=compressed_obj.recording,
index=segment['segment_index'],
defaults={
'contours': segment['contours'],
'bounding_box': Polygon(
(
(segment['start_ms'], segment['freq_max']),
(segment['stop_ms'], segment['freq_max']),
(segment['stop_ms'], segment['freq_min']),
(segment['start_ms'], segment['freq_min']),
(segment['start_ms'], segment['freq_max']),
)
),
},
)
segment_index_map[segment['segment_index']] = pulse_metadata_obj
for segment in compressed['segments']:
if segment['segment_index'] not in segment_index_map:
PulseMetadata.objects.update_or_create(
recording=compressed_obj.recording,
index=segment['segment_index'],
defaults={
'curve': LineString([Point(x[1], x[0]) for x in segment['curve_hz_ms']]),
'char_freq': Point(segment['char_freq_ms'], segment['char_freq_hz']),
'knee': Point(segment['knee_ms'], segment['knee_hz']),
'heel': Point(segment['heel_ms'], segment['heel_hz']),
},
)
else:
pulse_metadata_obj = segment_index_map[segment['segment_index']]
pulse_metadata_obj.curve = LineString(
[Point(x[1], x[0]) for x in segment['curve_hz_ms']]
)
pulse_metadata_obj.char_freq = Point(
segment['char_freq_ms'], segment['char_freq_hz']
)
pulse_metadata_obj.knee = Point(segment['knee_ms'], segment['knee_hz'])
pulse_metadata_obj.heel = Point(segment['heel_ms'], segment['heel_hz'])
pulse_metadata_obj.save()

processing_task.status = ProcessingTask.Status.COMPLETE
processing_task.save()
31 changes: 28 additions & 3 deletions bats_ai/core/tasks/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import tempfile

from django.contrib.contenttypes.models import ContentType
from django.contrib.gis.geos import Polygon
from django.contrib.gis.geos import LineString, Point, Polygon
from django.core.files import File

from bats_ai.celery import app
Expand Down Expand Up @@ -101,8 +101,9 @@ def recording_compute_spectrogram(recording_id: int):
)

# Create SpectrogramContour objects for each segment
for segment in results['segments']['segments']:
PulseMetadata.objects.update_or_create(
segment_index_map = {}
for segment in compressed['contours']['segments']:
pulse_metadata_obj, _ = PulseMetadata.objects.update_or_create(
recording=compressed_obj.recording,
index=segment['segment_index'],
defaults={
Expand All @@ -118,5 +119,29 @@ def recording_compute_spectrogram(recording_id: int):
),
},
)
segment_index_map[segment['segment_index']] = pulse_metadata_obj
for segment in compressed['segments']:
if segment['segment_index'] not in segment_index_map:
PulseMetadata.objects.update_or_create(
recording=compressed_obj.recording,
index=segment['segment_index'],
defaults={
'curve': LineString([Point(x[1], x[0]) for x in segment['curve_hz_ms']]),
'char_freq': Point(segment['char_freq_ms'], segment['char_freq_hz']),
'knee': Point(segment['knee_ms'], segment['knee_hz']),
'heel': Point(segment['heel_ms'], segment['heel_hz']),
},
)
else:
pulse_metadata_obj = segment_index_map[segment['segment_index']]
pulse_metadata_obj.curve = LineString(
[Point(x[1], x[0]) for x in segment['curve_hz_ms']]
)
pulse_metadata_obj.char_freq = Point(
segment['char_freq_ms'], segment['char_freq_hz']
)
pulse_metadata_obj.knee = Point(segment['knee_ms'], segment['knee_hz'])
pulse_metadata_obj.heel = Point(segment['heel_ms'], segment['heel_hz'])
pulse_metadata_obj.save()

return {'spectrogram_id': spectrogram.id, 'compressed_id': compressed_obj.id}
41 changes: 38 additions & 3 deletions bats_ai/core/utils/batbot_metadata.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import contextmanager
import json
import logging
import os
from pathlib import Path
from typing import Any, TypedDict
Expand All @@ -15,6 +16,8 @@

from .contour_utils import process_spectrogram_assets_for_contours

logger = logging.getLogger(__name__)


class SpectrogramMetadata(BaseModel):
"""Metadata about the spectrogram."""
Expand Down Expand Up @@ -261,6 +264,17 @@ class SpectrogramContourSegment(TypedDict):
stop_ms: float


class BatBotMetadataCurve(TypedDict):
segment_index: int
curve_hz_ms: list[float]
char_freq_ms: float
char_freq_hz: float
knee_ms: float
knee_hz: float
heel_ms: float
heel_hz: float


class SpectrogramContours(TypedDict):
segments: list[SpectrogramContourSegment]
total_segments: int
Expand All @@ -272,7 +286,7 @@ class SpectrogramAssets(TypedDict):
freq_max: int
normal: SpectrogramAssetResult
compressed: SpectrogramCompressedAssetResult
segments: SpectrogramContours | None
contours: SpectrogramContours | None


@contextmanager
Expand All @@ -285,6 +299,25 @@ def working_directory(path):
os.chdir(previous)


def convert_to_segment_data(
metadata: BatbotMetadata,
) -> list[BatBotMetadataCurve]:
segment_data: list[BatBotMetadataCurve] = []
for index, segment in enumerate(metadata.segments):
segment_data_item: BatBotMetadataCurve = {
'segment_index': index,
'curve_hz_ms': segment.curve_hz_ms,
'char_freq_ms': segment.fc_ms,
'char_freq_hz': segment.fc_hz,
'knee_ms': segment.hi_fc_knee_ms,
'knee_hz': segment.hi_fc_knee_hz,
'heel_ms': segment.lo_fc_heel_ms,
'heel_hz': segment.lo_fc_heel_hz,
}
segment_data.append(segment_data_item)
return segment_data


def generate_spectrogram_assets(recording_path: str, output_folder: str):
batbot.pipeline(recording_path, output_folder=output_folder)
# There should be a .metadata.json file in the output_base directory by replacing extentions
Expand All @@ -300,6 +333,7 @@ def generate_spectrogram_assets(recording_path: str, output_folder: str):
metadata.frequencies.max_hz

compressed_metadata = convert_to_compressed_spectrogram_data(metadata)
segment_curve_data = convert_to_segment_data(metadata)
result: SpectrogramAssets = {
'duration': metadata.duration_ms,
'freq_min': metadata.frequencies.min_hz,
Expand All @@ -317,10 +351,11 @@ def generate_spectrogram_assets(recording_path: str, output_folder: str):
'widths': compressed_metadata.widths,
'starts': compressed_metadata.starts,
'stops': compressed_metadata.stops,
'segments': segment_curve_data,
},
}

segments_data = process_spectrogram_assets_for_contours(result)
result['segments'] = segments_data
contour_segments_data = process_spectrogram_assets_for_contours(result)
result['compressed']['contours'] = contour_segments_data

return result
51 changes: 50 additions & 1 deletion bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ class UpdateAnnotationsSchema(Schema):
id: int | None


class PulseMetadataSchema(Schema):
class PulseContourSchema(Schema):
id: int | None
index: int
bounding_box: Any
Expand All @@ -145,6 +145,36 @@ def from_orm(cls, obj: PulseMetadata):
)


class PulseMetadataSchema(Schema):
id: int | None
index: int
curve: list[list[float]] | None = None # list of [time, frequency]
char_freq: list[float] | None = None # point [time, frequency]
knee: list[float] | None = None # point [time, frequency]
heel: list[float] | None = None # point [time, frequency]

@classmethod
def from_orm(cls, obj: PulseMetadata):
def point_to_list(pt):
if pt is None:
return None
return [pt.x, pt.y]

def linestring_to_list(ls):
if ls is None:
return None
return [[c[0], c[1]] for c in ls.coords]

return cls(
id=obj.id,
index=obj.index,
curve=linestring_to_list(obj.curve),
char_freq=point_to_list(obj.char_freq),
knee=point_to_list(obj.knee),
heel=point_to_list(obj.heel),
)


@router.post('/')
def create_recording(
request: HttpRequest,
Expand Down Expand Up @@ -559,6 +589,25 @@ def get_annotations(request: HttpRequest, id: int):
return {'error': 'Recording not found'}


@router.get('/{id}/pulse_contours')
def get_pulse_contours(request: HttpRequest, id: int):
try:
recording = Recording.objects.get(pk=id)
if recording.owner == request.user or recording.public:
computed_pulse_annotation_qs = PulseMetadata.objects.filter(
recording=recording
).order_by('index')
return [
PulseContourSchema.from_orm(pulse) for pulse in computed_pulse_annotation_qs.all()
]
else:
return {
'error': 'Permission denied. You do not own this recording, and it is not public.'
}
except Recording.DoesNotExist:
return {'error': 'Recording not found'}


@router.get('/{id}/pulse_data')
def get_pulse_data(request: HttpRequest, id: int):
try:
Expand Down
23 changes: 19 additions & 4 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,14 +574,28 @@ export interface Contour {
index: number;
}

export interface ComputedPulseAnnotation {
export interface ComputedPulseContour {
id: number;
index: number;
contours: Contour[];
}

async function getComputedPulseAnnotations(recordingId: number) {
const result = await axiosInstance.get<ComputedPulseAnnotation[]>(`/recording/${recordingId}/pulse_data`);
async function getComputedPulseContour(recordingId: number) {
const result = await axiosInstance.get<ComputedPulseContour[]>(`/recording/${recordingId}/pulse_contours`);
return result.data;
}

export interface PulseMetadata {
id: number;
index: number;
curve: number[][] | null; // list of [time, frequency]
char_freq: number[] | null; // point [time, frequency]
knee: number[] | null; // point [time, frequency]
heel: number[] | null; // point [time, frequency]
}

async function getPulseMetadata(recordingId: number) {
const result = await axiosInstance.get<PulseMetadata[]>(`/recording/${recordingId}/pulse_data`);
return result.data;
}

Expand Down Expand Up @@ -622,7 +636,8 @@ export {
getFileAnnotationDetails,
getExportStatus,
getRecordingTags,
getComputedPulseAnnotations,
getComputedPulseContour,
getPulseMetadata,
getCurrentUser,
getVettingDetailsForUser,
createOrUpdateVettingDetailsForUser,
Expand Down
Loading