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
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
Loading