Skip to content

Commit

Permalink
Add recording annnotation and migrations
Browse files Browse the repository at this point in the history
  • Loading branch information
BryonLewis committed Dec 2, 2024
1 parent 63c0dcd commit c145a9c
Show file tree
Hide file tree
Showing 8 changed files with 281 additions and 3 deletions.
9 changes: 8 additions & 1 deletion bats_ai/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
from ninja import NinjaAPI
from oauth2_provider.models import AccessToken

from bats_ai.core.views import GRTSCellsRouter, GuanoMetadataRouter, RecordingRouter, SpeciesRouter
from bats_ai.core.views import (
GRTSCellsRouter,
GuanoMetadataRouter,
RecordingAnnotationRouter,
RecordingRouter,
SpeciesRouter,
)

logger = logging.getLogger(__name__)

Expand All @@ -28,3 +34,4 @@ def global_auth(request):
api.add_router('/species/', SpeciesRouter)
api.add_router('/grts/', GRTSCellsRouter)
api.add_router('/guano/', GuanoMetadataRouter)
api.add_router('/recording-annotation/', RecordingAnnotationRouter)
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Generated by Django 4.1.13 on 2024-12-02 15:22

from django.conf import settings
import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import django.utils.timezone
import django_extensions.db.fields


class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('core', '0010_compressedspectrogram'),
]

operations = [
migrations.AlterModelOptions(
name='annotations',
options={'get_latest_by': 'modified'},
),
migrations.AddField(
model_name='annotations',
name='confidence',
field=models.FloatField(blank=True, null=True),
),
migrations.AddField(
model_name='annotations',
name='created',
field=django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, default=django.utils.timezone.now, verbose_name='created'
),
preserve_default=False,
),
migrations.AddField(
model_name='annotations',
name='model',
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='annotations',
name='modified',
field=django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name='modified'
),
),
migrations.CreateModel(
name='RecordingAnnotation',
fields=[
(
'id',
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name='ID'
),
),
(
'created',
django_extensions.db.fields.CreationDateTimeField(
auto_now_add=True, verbose_name='created'
),
),
(
'modified',
django_extensions.db.fields.ModificationDateTimeField(
auto_now=True, verbose_name='modified'
),
),
('comments', models.TextField(blank=True, null=True)),
('model', models.TextField(blank=True, null=True)),
(
'confidence',
models.FloatField(
default=1.0,
help_text='A confidence value between 0 and 1.0, default is 1.0.',
validators=[
django.core.validators.MinValueValidator(0.0),
django.core.validators.MaxValueValidator(1.0),
],
),
),
(
'owner',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
(
'recording',
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to='core.recording'
),
),
('species', models.ManyToManyField(to='core.species')),
],
options={
'get_latest_by': 'modified',
'abstract': False,
},
),
]
2 changes: 2 additions & 0 deletions bats_ai/core/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .grts_cells import GRTSCells
from .image import Image
from .recording import Recording, colormap
from .recording_annotation import RecordingAnnotation
from .recording_annotation_status import RecordingAnnotationStatus
from .species import Species
from .spectrogram import Spectrogram
Expand All @@ -19,4 +20,5 @@
'GRTSCells',
'colormap',
'CompressedSpectrogram',
'RecordingAnnotation',
]
13 changes: 12 additions & 1 deletion bats_ai/core/models/annotations.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django_extensions.db.models import TimeStampedModel

from .recording import Recording
from .species import Species


class Annotations(models.Model):
class Annotations(TimeStampedModel, models.Model):
recording = models.ForeignKey(Recording, on_delete=models.CASCADE)
owner = models.ForeignKey(User, on_delete=models.CASCADE)
start_time = models.IntegerField(blank=True, null=True)
Expand All @@ -15,3 +17,12 @@ class Annotations(models.Model):
type = models.TextField(blank=True, null=True)
species = models.ManyToManyField(Species)
comments = models.TextField(blank=True, null=True)
model = models.TextField(blank=True, null=True) # AI Model information if inference used
confidence = models.FloatField(
default=1.0,
validators=[
MinValueValidator(0.0),
MaxValueValidator(1.0),
],
help_text='A confidence value between 0 and 1.0, default is 1.0.',
)
13 changes: 12 additions & 1 deletion bats_ai/core/models/recording_annotation.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
from django.contrib.auth.models import User
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django_extensions.db.models import TimeStampedModel

from .recording import Recording
from .species import Species


class RecordingAnnotation(models.Model):
class RecordingAnnotation(TimeStampedModel, models.Model):
recording = models.ForeignKey(Recording, on_delete=models.CASCADE)
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
confidence = models.FloatField(
default=1.0,
validators=[
MinValueValidator(0.0),
MaxValueValidator(1.0),
],
help_text='A confidence value between 0 and 1.0, default is 1.0.',
)
2 changes: 2 additions & 0 deletions bats_ai/core/views/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from .grts_cells import router as GRTSCellsRouter
from .guanometadata import router as GuanoMetadataRouter
from .recording import router as RecordingRouter
from .recording_annotation import router as RecordingAnnotationRouter
from .species import router as SpeciesRouter
from .temporal_annotations import router as TemporalAnnotationRouter

Expand All @@ -12,4 +13,5 @@
'TemporalAnnotationRouter',
'GRTSCellsRouter',
'GuanoMetadataRouter',
'RecordingAnnotationRouter',
]
5 changes: 5 additions & 0 deletions bats_ai/core/views/recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
Annotations,
CompressedSpectrogram,
Recording,
RecordingAnnotation,
Species,
TemporalAnnotations,
colormap,
Expand Down Expand Up @@ -215,6 +216,8 @@ def get_recordings(request: HttpRequest, public: bool | None = None):
# TODO with larger dataset it may be better to do this in a queryset instead of python
for recording in recordings:
user = User.objects.get(id=recording['owner_id'])
fileAnnotations = RecordingAnnotation.objects.filter(recording=recording['id'])
recording['fileAnnotations'] = fileAnnotations
recording['owner_username'] = user.username
recording['audio_file_presigned_url'] = default_storage.url(recording['audio_file'])
recording['hasSpectrogram'] = Recording.objects.get(id=recording['id']).has_spectrogram
Expand Down Expand Up @@ -260,6 +263,8 @@ def get_recording(request: HttpRequest, id: int):
recording_id=recording['id'], owner=request.user
).exists()
recording['userMadeAnnotations'] = user_has_annotations
annotations = RecordingAnnotation.objects.filter(recording=id).annotate('confidence')
recording['fileAnnotations'] = annotations

return recording
else:
Expand Down
140 changes: 140 additions & 0 deletions bats_ai/core/views/recording_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import logging

from django.http import HttpRequest
from ninja import Router, Schema
from ninja.errors import HttpError

from bats_ai.core.models import Recording, RecordingAnnotation, Species

logger = logging.getLogger(__name__)

router = Router()


# Schemas for serialization
class RecordingAnnotationSchema(Schema):
id: int
recording: int
owner: str
species: list[int]
comments: str = None
model: str = None
confidence: float


class CreateRecordingAnnotationSchema(Schema):
recording: int
species: list[int]
comments: str = None
model: str = None
confidence: float


class UpdateRecordingAnnotationSchema(Schema):
species: list[int] = None
comments: str = None
model: str = None
confidence: float = None


# GET Endpoint
@router.get('/{id}', response=RecordingAnnotationSchema)
def get_recording_annotation(request: HttpRequest, id: int):
try:
annotation = RecordingAnnotation.objects.get(pk=id)

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

return {
'id': annotation.id,
'recording': annotation.recording.id,
'owner': annotation.owner.username,
'species': [s.id for s in annotation.species.all()],
'comments': annotation.comments,
'model': annotation.model,
'confidence': annotation.confidence,
}
except RecordingAnnotation.DoesNotExist:
raise HttpError(404, 'Recording annotation not found.')


# PUT Endpoint
@router.put('/', response={200: str})
def create_recording_annotation(request: HttpRequest, data: CreateRecordingAnnotationSchema):
try:
recording = Recording.objects.get(pk=data.recording)

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

# Create the recording annotation
annotation = RecordingAnnotation.objects.create(
recording=recording,
owner=request.user,
comments=data.comments,
model=data.model,
confidence=data.confidence,
)

# Add species
for species_id in data.species:
species = Species.objects.get(pk=species_id)
annotation.species.add(species)

return 'Recording annotation created successfully.'
except Recording.DoesNotExist:
raise HttpError(404, 'Recording not found.')
except Species.DoesNotExist:
raise HttpError(404, 'One or more species IDs not found.')


# PATCH Endpoint
@router.patch('/{id}', response={200: str})
def update_recording_annotation(
request: HttpRequest, id: int, data: UpdateRecordingAnnotationSchema
):
try:
annotation = RecordingAnnotation.objects.get(pk=id)

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

# Update fields if provided
if data.comments is not None:
annotation.comments = data.comments
if data.model is not None:
annotation.model = data.model
if data.confidence is not None:
annotation.confidence = data.confidence
if data.species is not None:
annotation.species.clear() # Clear existing species
for species_id in data.species:
species = Species.objects.get(pk=species_id)
annotation.species.add(species)

annotation.save()
return 'Recording annotation updated successfully.'
except RecordingAnnotation.DoesNotExist:
raise HttpError(404, 'Recording annotation not found.')
except Species.DoesNotExist:
raise HttpError(404, 'One or more species IDs not found.')


# DELETE Endpoint
@router.delete('/{id}', response={200: str})
def delete_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.delete()
return 'Recording annotation deleted successfully.'
except RecordingAnnotation.DoesNotExist:
raise HttpError(404, 'Recording annotation not found.')

0 comments on commit c145a9c

Please sign in to comment.