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
1 change: 1 addition & 0 deletions bats_ai/core/admin/recording_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class RecordingAnnotationAdmin(admin.ModelAdmin):
'additional_data',
'comments',
'model',
'submitted',
]
list_select_related = True
filter_horizontal = ('species',) # or filter_vertical
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 4.2.23 on 2025-12-23 20:18

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('core', '0023_recordingtag_recording_tags_and_more'),
]

operations = [
migrations.AddField(
model_name='configuration',
name='mark_annotations_completed_enabled',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='configuration',
name='non_admin_upload_enabled',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='configuration',
name='show_my_recordings',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='recordingannotation',
name='submitted',
field=models.BooleanField(default=False),
),
]
5 changes: 5 additions & 0 deletions bats_ai/core/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class AvailableColorScheme(models.TextChoices):
# 18 characters is just enough for "rgb(255, 255, 255)"
default_spectrogram_background_color = models.CharField(max_length=18, default='rgb(0, 0, 0)')

# Fields used for community vetting focused deployment of BatAI
non_admin_upload_enabled = models.BooleanField(default=True)
mark_annotations_completed_enabled = models.BooleanField(default=False)
show_my_recordings = models.BooleanField(default=True)

def save(self, *args, **kwargs):
# Ensure only one instance of Configuration exists
if not Configuration.objects.exists() and not self.pk:
Expand Down
4 changes: 3 additions & 1 deletion bats_ai/core/models/recording_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class RecordingAnnotation(TimeStampedModel, models.Model):
owner = models.ForeignKey(User, on_delete=models.CASCADE)
species = models.ManyToManyField(Species)
comments = models.TextField(blank=True, null=True)
model = models.TextField(blank=True, null=True) # AI Model information if inference used
# AI Model information if inference used, else "User Defined"
model = models.TextField(blank=True, null=True)
confidence = models.FloatField(
default=1.0,
validators=[
Expand All @@ -24,3 +25,4 @@ class RecordingAnnotation(TimeStampedModel, models.Model):
additional_data = models.JSONField(
blank=True, null=True, help_text='Additional information about the models/data'
)
submitted = models.BooleanField(default=False)
16 changes: 16 additions & 0 deletions bats_ai/core/views/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ class ConfigurationSchema(Schema):
spectrogram_view: Configuration.SpectrogramViewMode
default_color_scheme: Configuration.AvailableColorScheme
default_spectrogram_background_color: str
non_admin_upload_enabled: bool
mark_annotations_completed_enabled: bool
show_my_recordings: bool


# Endpoint to retrieve the configuration status
Expand All @@ -38,6 +41,9 @@ def get_configuration(request):
spectrogram_view=config.spectrogram_view,
default_color_scheme=config.default_color_scheme,
default_spectrogram_background_color=config.default_spectrogram_background_color,
non_admin_upload_enabled=config.non_admin_upload_enabled,
mark_annotations_completed_enabled=config.mark_annotations_completed_enabled,
show_my_recordings=config.show_my_recordings,
is_admin=request.user.is_authenticated and request.user.is_superuser,
)

Expand All @@ -61,3 +67,13 @@ def check_is_admin(request):
if request.user.is_authenticated:
return {'is_admin': request.user.is_superuser}
return {'is_admin': False}


@router.get('/me')
def get_current_user(request):
if request.user.is_authenticated:
return {
'email': request.user.email,
'name': request.user.username,
}
return {'email': '', 'name': ''}
16 changes: 15 additions & 1 deletion bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ class RecordingAnnotationSchema(Schema):
confidence: float
id: int | None = None
hasDetails: bool
submitted: bool

@classmethod
def from_orm(cls, obj: RecordingAnnotation, **kwargs):
Expand All @@ -87,6 +88,7 @@ def from_orm(cls, obj: RecordingAnnotation, **kwargs):
model=obj.model,
id=obj.pk,
hasDetails=obj.additional_data is not None,
submitted=obj.submitted,
)


Expand Down Expand Up @@ -246,7 +248,9 @@ def delete_recording(


@router.get('/')
def get_recordings(request: HttpRequest, public: bool | None = None):
def get_recordings(
request: HttpRequest, public: bool | None = None, exclude_submitted: bool | None = None
):
# Filter recordings based on the owner's id or public=True
if public is not None and public:
recordings = (
Expand Down Expand Up @@ -290,6 +294,16 @@ def get_recordings(request: HttpRequest, public: bool | None = None):
)
recording['userMadeAnnotations'] = user_has_annotations

if exclude_submitted:
recordings = [
recording
for recording in recordings
if not any(
annotation['submitted'] and annotation['owner'] == request.user.username
for annotation in recording['fileAnnotations']
)
]

return list(recordings)


Expand Down
36 changes: 35 additions & 1 deletion bats_ai/core/views/recording_annotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from ninja import Router, Schema
from ninja.errors import HttpError

from bats_ai.core.models import Recording, RecordingAnnotation, Species
from bats_ai.core.models import Configuration, Recording, RecordingAnnotation, Species
from bats_ai.core.views.recording import SpeciesSchema

logger = logging.getLogger(__name__)
Expand All @@ -20,6 +20,7 @@ class RecordingAnnotationSchema(Schema):
owner: str
confidence: float
id: int | None = None
submitted: bool
hasDetails: bool

@classmethod
Expand All @@ -32,9 +33,11 @@ def from_orm(cls, obj: RecordingAnnotation, **kwargs):
model=obj.model,
id=obj.pk,
hasDetails=obj.additional_data is not None,
submitted=obj.submitted,
)


# TODO: do we really need this? why can't we just always return the details?
class RecordingAnnotationDetailsSchema(Schema):
species: list[SpeciesSchema] | None
comments: str | None = None
Expand All @@ -44,6 +47,7 @@ class RecordingAnnotationDetailsSchema(Schema):
id: int | None = None
details: dict
hasDetails: bool
submitted: bool

@classmethod
def from_orm(cls, obj: RecordingAnnotation, **kwargs):
Expand All @@ -56,6 +60,7 @@ def from_orm(cls, obj: RecordingAnnotation, **kwargs):
hasDetails=obj.additional_data is not None,
details=obj.additional_data,
id=obj.pk,
submitted=obj.submitted,
)


Expand Down Expand Up @@ -168,6 +173,15 @@ def update_recording_annotation(
@router.delete('/{id}', response={200: str})
def delete_recording_annotation(request: HttpRequest, id: int):
try:
configuration = Configuration.objects.first()
vetting_enabled = (
configuration.mark_annotations_completed_enabled if configuration else False
)
if vetting_enabled and not request.user.is_staff:
raise HttpError(
403, 'Permission denied. Annotations cannot be deleted while vetting is enabled'
)

annotation = RecordingAnnotation.objects.get(pk=id)

# Check permission
Expand All @@ -178,3 +192,23 @@ def delete_recording_annotation(request: HttpRequest, id: int):
return 'Recording annotation deleted successfully.'
except RecordingAnnotation.DoesNotExist:
raise HttpError(404, 'Recording annotation not found.')


# Submit endpoint
@router.patch('/{id}/submit', response={200: dict})
def submit_recording_annotation(request: HttpRequest, id: int):
try:
annotation = RecordingAnnotation.objects.get(pk=id)

# Check permission
if annotation.recording.owner != request.user:
raise HttpError(403, 'Permission denied.')

annotation.submitted = True
annotation.save()
return {
'id': id,
'submitted': annotation.submitted,
}
except RecordingAnnotation.DoesNotExist:
raise HttpError(404, 'Recording annotation not found.')
16 changes: 16 additions & 0 deletions client/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ export interface FileAnnotation {
confidence: number;
hasDetails: boolean;
id: number;
submitted: boolean;
}

export interface FileAnnotationDetails {
Expand Down Expand Up @@ -395,6 +396,12 @@ async function deleteFileAnnotation(fileAnnotationId: number) {
);
}

async function submitFileAnnotation(fileAnnotationId: number) {
return axiosInstance.patch<{ id: number, submitted: boolean }>(
`recording-annotation/${fileAnnotationId}/submit`
);
}

interface CellIDReponse {
grid_cell_id?: number;
error?: string;
Expand All @@ -414,6 +421,9 @@ export interface ConfigurationSettings {
is_admin?: boolean;
default_color_scheme: string;
default_spectrogram_background_color: string;
non_admin_upload_enabled: boolean;
mark_annotations_completed_enabled: boolean;
show_my_recordings: boolean;
}

export type Configuration = ConfigurationSettings & { is_admin: boolean };
Expand All @@ -425,6 +435,10 @@ async function patchConfiguration(config: ConfigurationSettings) {
return axiosInstance.patch("/configuration/", { ...config });
}

async function getCurrentUser() {
return axiosInstance.get<{name: string, email: string}>("/configuration/me");
}

export interface ProcessingTask {
id: number;
created: string;
Expand Down Expand Up @@ -531,6 +545,7 @@ export {
putFileAnnotation,
patchFileAnnotation,
deleteFileAnnotation,
submitFileAnnotation,
getConfiguration,
patchConfiguration,
getProcessingTasks,
Expand All @@ -540,4 +555,5 @@ export {
getFileAnnotationDetails,
getExportStatus,
getRecordingTags,
getCurrentUser,
};
Loading