Skip to content

Commit

Permalink
Add File Level Annotations (#111)
Browse files Browse the repository at this point in the history
* adding recording annotation model

* Add recording annnotation and migrations

* client-side updates

* adding recording annotation interface

* basic adding/editing user based file level annotations

* adding file annotation to main recording view

* linting client

* change apt-fast to apt-get

* revert apt-fast

* convert to sudo apt-get

* pin to ubuntu 22.04
  • Loading branch information
BryonLewis authored Dec 9, 2024
1 parent 6b974cf commit 2aa32ef
Show file tree
Hide file tree
Showing 21 changed files with 1,200 additions and 5,177 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ env:
jobs:
lint-python:
name: Lint Python
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
working-directory: client
test-django:
name: Test Django [${{ matrix.tox-env }}]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -92,7 +92,7 @@ jobs:
- name: Update Package References
run: sudo apt-get update
- name: Install system dependencies
run: apt-fast install --no-install-recommends --yes
run: sudo apt-get install --no-install-recommends --yes
libgdal30
libproj22
python3-cachecontrol
Expand All @@ -110,7 +110,7 @@ jobs:
working-directory: bats_ai
test-vue:
name: Test [vue]
runs-on: ubuntu-latest
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v4
Expand Down
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,107 @@
# Generated by Django 4.1.13 on 2024-12-09 15:26

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(
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),
],
),
),
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.',
)
23 changes: 23 additions & 0 deletions bats_ai/core/models/recording_annotation.py
Original file line number Diff line number Diff line change
@@ -0,0 +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(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',
]
82 changes: 72 additions & 10 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 @@ -61,6 +62,26 @@ class RecordingUploadSchema(Schema):
unusual_occurrences: str = None


class RecordingAnnotationSchema(Schema):
species: list[SpeciesSchema] | None
comments: str | None = None
model: str | None = None
owner: str
confidence: float
id: int | None = None

@classmethod
def from_orm(cls, obj: RecordingAnnotation, **kwargs):
return cls(
species=[SpeciesSchema.from_orm(species) for species in obj.species.all()],
owner=obj.owner.username,
confidence=obj.confidence,
comments=obj.comments,
model=obj.model,
id=obj.pk,
)


class AnnotationSchema(Schema):
start_time: int
end_time: int
Expand All @@ -73,7 +94,7 @@ class AnnotationSchema(Schema):
owner_email: str = None

@classmethod
def from_orm(cls, obj, owner_email=None, **kwargs):
def from_orm(cls, obj: Annotations, owner_email=None, **kwargs):
return cls(
start_time=obj.start_time,
end_time=obj.end_time,
Expand Down Expand Up @@ -215,6 +236,11 @@ 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'] = [
RecordingAnnotationSchema.from_orm(fileAnnotation).dict()
for fileAnnotation in 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 All @@ -227,9 +253,12 @@ def get_recordings(request: HttpRequest, public: bool | None = None):
.count()
)
recording['userAnnotations'] = unique_users_with_annotations
user_has_annotations = Annotations.objects.filter(
recording_id=recording['id'], owner=request.user
).exists()
user_has_annotations = (
Annotations.objects.filter(recording_id=recording['id'], owner=request.user).exists()
or RecordingAnnotation.objects.filter(
recording_id=recording['id'], owner=request.user
).exists()
)
recording['userMadeAnnotations'] = user_has_annotations

return list(recordings)
Expand All @@ -249,17 +278,38 @@ def get_recording(request: HttpRequest, id: int):
recording['hasSpectrogram'] = Recording.objects.get(id=recording['id']).has_spectrogram
if recording['recording_location']:
recording['recording_location'] = json.loads(recording['recording_location'].json)
unique_users_with_annotations = (
annotation_owners = (
Annotations.objects.filter(recording_id=recording['id'])
.values('owner')
.values_list('owner', flat=True)
.distinct()
.count()
)
recording_annotation_owners = (
RecordingAnnotation.objects.filter(recording_id=recording['id'])
.values_list('owner', flat=True)
.distinct()
)

# Combine the sets of owners and count unique entries
unique_users_with_annotations = len(
set(annotation_owners).union(set(recording_annotation_owners))
)
recording['userAnnotations'] = unique_users_with_annotations
user_has_annotations = Annotations.objects.filter(
recording_id=recording['id'], owner=request.user
).exists()
user_has_annotations = (
Annotations.objects.filter(
recording_id=recording['id'], owner=request.user
).exists()
or RecordingAnnotation.objects.filter(
recording_id=recording['id'], owner=request.user
).exists()
)
recording['userMadeAnnotations'] = user_has_annotations
fileAnnotations = RecordingAnnotation.objects.filter(recording=id).order_by(
'confidence'
)
recording['fileAnnotations'] = [
RecordingAnnotationSchema.from_orm(fileAnnotation).dict()
for fileAnnotation in fileAnnotations
]

return recording
else:
Expand All @@ -268,6 +318,18 @@ def get_recording(request: HttpRequest, id: int):
return {'error': 'Recording not found'}


@router.get('/{recording_id}/recording-annotations')
def get_recording_annotations(request: HttpRequest, recording_id: int):
fileAnnotations = RecordingAnnotation.objects.filter(recording=recording_id).order_by(
'confidence'
)
output = [
RecordingAnnotationSchema.from_orm(fileAnnotation).dict()
for fileAnnotation in fileAnnotations
]
return output


@router.get('/{id}/spectrogram')
def get_spectrogram(request: HttpRequest, id: int):
try:
Expand Down
Loading

0 comments on commit 2aa32ef

Please sign in to comment.