From 8667bcbf7f8b0f283192911114da3e4725143681 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Mon, 1 Apr 2024 14:39:22 -0400 Subject: [PATCH 1/8] initial metadata extraction --- bats_ai/api.py | 3 +- bats_ai/core/views/__init__.py | 3 +- bats_ai/core/views/guanometadata.py | 114 ++++++++++++++++++++++++++++ scripts/guanoInfo.py | 27 +++++++ setup.py | 1 + 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 bats_ai/core/views/guanometadata.py create mode 100644 scripts/guanoInfo.py diff --git a/bats_ai/api.py b/bats_ai/api.py index 2ec2f83..9528210 100644 --- a/bats_ai/api.py +++ b/bats_ai/api.py @@ -3,7 +3,7 @@ from ninja import NinjaAPI from oauth2_provider.models import AccessToken -from bats_ai.core.views import GRTSCellsRouter, RecordingRouter, SpeciesRouter +from bats_ai.core.views import GRTSCellsRouter, RecordingRouter, SpeciesRouter, GuanoMetadataRouter logger = logging.getLogger(__name__) @@ -27,3 +27,4 @@ def global_auth(request): api.add_router('/recording/', RecordingRouter) api.add_router('/species/', SpeciesRouter) api.add_router('/grts/', GRTSCellsRouter) +api.add_router('/guano/', GuanoMetadataRouter) diff --git a/bats_ai/core/views/__init__.py b/bats_ai/core/views/__init__.py index f503fcc..189084c 100644 --- a/bats_ai/core/views/__init__.py +++ b/bats_ai/core/views/__init__.py @@ -3,11 +3,12 @@ from .recording import router as RecordingRouter from .species import router as SpeciesRouter from .temporal_annotations import router as TemporalAnnotationRouter - +from .guanometadata import router as GuanoMetadataRouter __all__ = [ 'RecordingRouter', 'SpeciesRouter', 'AnnotationRouter', 'TemporalAnnotationRouter', 'GRTSCellsRouter', + 'GuanoMetadataRouter', ] diff --git a/bats_ai/core/views/guanometadata.py b/bats_ai/core/views/guanometadata.py new file mode 100644 index 0000000..6256395 --- /dev/null +++ b/bats_ai/core/views/guanometadata.py @@ -0,0 +1,114 @@ +from datetime import datetime +import json +import logging + +from django.contrib.auth.models import User +from django.contrib.gis.geos import Point +from django.core.files.storage import default_storage +from django.db.models import Q +from django.http import HttpRequest, JsonResponse +from ninja import File, Form, Schema +from ninja.files import UploadedFile +from ninja.pagination import RouterPaginated + +from bats_ai.core.models import Annotations, Recording, Species, TemporalAnnotations +from bats_ai.core.tasks import recording_compute_spectrogram +from bats_ai.core.views.species import SpeciesSchema +from bats_ai.core.views.temporal_annotations import ( + TemporalAnnotationSchema, + UpdateTemporalAnnotationSchema, +) + +from guano import GuanoFile + +router = RouterPaginated() + + +class GuanoMetadataSchema(Schema): + nabat_grid_cell_grts_id: str + nabat_latitude: float + nabat_longitude: float + nabat_site_name: str + nabat_activation_start_time: datetime + nabat_activation_end_time: datetime + nabat_software_type: str + nabat_species_list: list[str] + nabat_comments: str + nabat_detector_type: str + nabat_unusual_occurrences: str + +from datetime import datetime +import json +import logging + +from django.contrib.auth.models import User +from django.contrib.gis.geos import Point +from django.core.files.storage import default_storage +from django.db.models import Q +from django.http import HttpRequest, JsonResponse +from ninja import File, Form, Schema +from ninja.files import UploadedFile +from ninja.pagination import RouterPaginated + +from bats_ai.core.models import Annotations, Recording, Species, TemporalAnnotations +from bats_ai.core.tasks import recording_compute_spectrogram +from bats_ai.core.views.species import SpeciesSchema +from bats_ai.core.views.temporal_annotations import ( + TemporalAnnotationSchema, + UpdateTemporalAnnotationSchema, +) + +from guano import GuanoFile + +router = RouterPaginated() + + +class GuanoMetadataSchema(Schema): + nabat_grid_cell_grts_id: str + nabat_latitude: float + nabat_longitude: float + nabat_site_name: str + nabat_activation_start_time: datetime + nabat_activation_end_time: datetime + nabat_software_type: str + nabat_species_list: list[str] + nabat_comments: str + nabat_detector_type: str + nabat_unusual_occurrences: str + +@router.post('/') +def default_data( + request: HttpRequest, + audio_file: File[UploadedFile], +): + try: + # Read GUANO metadata from the file name provided + gfile = GuanoFile(audio_file.file.name) + + # Extract required NABat fields + nabat_fields = { + 'nabat_grid_cell_grts_id': gfile.get('NABat|Grid Cell GRTS ID', ''), + 'nabat_latitude': float(gfile.get('NABat|Latitude', 0)), + 'nabat_longitude': float(gfile.get('NABat|Longitude', 0)), + 'nabat_site_name': gfile.get('NABat|Site Name', ''), + } + + # Extract additional fields with conditionals + additional_fields = { + 'nabat_activation_start_time': datetime.strptime(gfile.get('NABat|Activation start time', ''), '%Y%m%dT%H%M%S') if 'NABat|Activation start time' in gfile else None, + 'nabat_activation_end_time': datetime.strptime(gfile.get('NABat|Activation end time', ''), '%Y%m%dT%H%M%S') if 'NABat|Activation end time' in gfile else None, + 'nabat_software_type': gfile.get('NABat|Software type', ''), + 'nabat_species_list': gfile.get('NABat|Species List', '').split(','), + 'nabat_comments': gfile.get('NABat|Comments', ''), + 'nabat_detector_type': gfile.get('NABat|Detector type', ''), + 'nabat_unusual_occurrences': gfile.get('NABat|Unusual occurrences', ''), + } + + # Combine all extracted fields + metadata = {**nabat_fields, **additional_fields} + + + return JsonResponse(metadata, safe=False) + + except Exception as e: + return JsonResponse({'error': str(e)}, status=500) diff --git a/scripts/guanoInfo.py b/scripts/guanoInfo.py new file mode 100644 index 0000000..ca98dfc --- /dev/null +++ b/scripts/guanoInfo.py @@ -0,0 +1,27 @@ +import click +import json +import guano + +@click.command() +@click.argument('input_file', type=click.Path(exists=True)) +@click.option('--output', '-o', default='output.json', help='Output JSON file path') +def extract_guano_metadata(input_file, output): + try: + # Load a .WAV file with (or without) GUANO metadata + g = guano.GuanoFile(input_file) + # Get and set metadata values like a Python dict + print(f"GUANO Version: {g['GUANO|Version']}") + # Print all the metadata values + print("All Metadata:") + for key, value in g.items(): + print(f"{key}: {value}") + + # Write the updated .WAV file back to disk + + click.echo(f"GUANO metadata extracted from {input_file} and saved to {output}") + + except Exception as e: + click.echo(f"Error: {e}") + +if __name__ == '__main__': + extract_guano_metadata() diff --git a/setup.py b/setup.py index 6589de7..8825292 100644 --- a/setup.py +++ b/setup.py @@ -57,6 +57,7 @@ 'matplotlib', 'numpy', 'opencv-python-headless', + 'guano', ], extras_require={ 'dev': [ From 56d9dfa073068655227a649f931374c4b8b20c5b Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Tue, 2 Apr 2024 15:37:08 -0400 Subject: [PATCH 2/8] add models to recording --- bats_ai/api.py | 2 +- bats_ai/core/models/recording.py | 8 +++ bats_ai/core/views/__init__.py | 3 +- bats_ai/core/views/guanometadata.py | 77 +++++++---------------------- scripts/guanoInfo.py | 19 +++---- 5 files changed, 38 insertions(+), 71 deletions(-) diff --git a/bats_ai/api.py b/bats_ai/api.py index 9528210..88ab5cd 100644 --- a/bats_ai/api.py +++ b/bats_ai/api.py @@ -3,7 +3,7 @@ from ninja import NinjaAPI from oauth2_provider.models import AccessToken -from bats_ai.core.views import GRTSCellsRouter, RecordingRouter, SpeciesRouter, GuanoMetadataRouter +from bats_ai.core.views import GRTSCellsRouter, GuanoMetadataRouter, RecordingRouter, SpeciesRouter logger = logging.getLogger(__name__) diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 19cf559..600687c 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -2,6 +2,8 @@ from django.contrib.gis.db import models from django_extensions.db.models import TimeStampedModel +from .species import Species + # TimeStampedModel also provides "created" and "modified" fields class Recording(TimeStampedModel, models.Model): @@ -16,6 +18,12 @@ class Recording(TimeStampedModel, models.Model): grts_cell_id = models.IntegerField(blank=True, null=True) grts_cell = models.IntegerField(blank=True, null=True) public = models.BooleanField(default=False) + software = models.TextField(blank=True, null=True) + detector = models.TextField(blank=True, null=True) + computed_species = models.ManyToManyField(Species) # species from a computed sense + official_species = models.ManyToManyField( + Species + ) # species that are detemrined by the owner or from annotations as official species list @property def has_spectrogram(self): diff --git a/bats_ai/core/views/__init__.py b/bats_ai/core/views/__init__.py index 189084c..90163e3 100644 --- a/bats_ai/core/views/__init__.py +++ b/bats_ai/core/views/__init__.py @@ -1,9 +1,10 @@ from .annotations import router as AnnotationRouter from .grts_cells import router as GRTSCellsRouter +from .guanometadata import router as GuanoMetadataRouter from .recording import router as RecordingRouter from .species import router as SpeciesRouter from .temporal_annotations import router as TemporalAnnotationRouter -from .guanometadata import router as GuanoMetadataRouter + __all__ = [ 'RecordingRouter', 'SpeciesRouter', diff --git a/bats_ai/core/views/guanometadata.py b/bats_ai/core/views/guanometadata.py index 6256395..8759214 100644 --- a/bats_ai/core/views/guanometadata.py +++ b/bats_ai/core/views/guanometadata.py @@ -1,26 +1,11 @@ from datetime import datetime -import json -import logging -from django.contrib.auth.models import User -from django.contrib.gis.geos import Point -from django.core.files.storage import default_storage -from django.db.models import Q from django.http import HttpRequest, JsonResponse -from ninja import File, Form, Schema +from guano import GuanoFile +from ninja import File, Schema from ninja.files import UploadedFile from ninja.pagination import RouterPaginated -from bats_ai.core.models import Annotations, Recording, Species, TemporalAnnotations -from bats_ai.core.tasks import recording_compute_spectrogram -from bats_ai.core.views.species import SpeciesSchema -from bats_ai.core.views.temporal_annotations import ( - TemporalAnnotationSchema, - UpdateTemporalAnnotationSchema, -) - -from guano import GuanoFile - router = RouterPaginated() @@ -37,45 +22,10 @@ class GuanoMetadataSchema(Schema): nabat_detector_type: str nabat_unusual_occurrences: str -from datetime import datetime -import json -import logging - -from django.contrib.auth.models import User -from django.contrib.gis.geos import Point -from django.core.files.storage import default_storage -from django.db.models import Q -from django.http import HttpRequest, JsonResponse -from ninja import File, Form, Schema -from ninja.files import UploadedFile -from ninja.pagination import RouterPaginated - -from bats_ai.core.models import Annotations, Recording, Species, TemporalAnnotations -from bats_ai.core.tasks import recording_compute_spectrogram -from bats_ai.core.views.species import SpeciesSchema -from bats_ai.core.views.temporal_annotations import ( - TemporalAnnotationSchema, - UpdateTemporalAnnotationSchema, -) - -from guano import GuanoFile router = RouterPaginated() -class GuanoMetadataSchema(Schema): - nabat_grid_cell_grts_id: str - nabat_latitude: float - nabat_longitude: float - nabat_site_name: str - nabat_activation_start_time: datetime - nabat_activation_end_time: datetime - nabat_software_type: str - nabat_species_list: list[str] - nabat_comments: str - nabat_detector_type: str - nabat_unusual_occurrences: str - @router.post('/') def default_data( request: HttpRequest, @@ -84,7 +34,7 @@ def default_data( try: # Read GUANO metadata from the file name provided gfile = GuanoFile(audio_file.file.name) - + # Extract required NABat fields nabat_fields = { 'nabat_grid_cell_grts_id': gfile.get('NABat|Grid Cell GRTS ID', ''), @@ -92,23 +42,30 @@ def default_data( 'nabat_longitude': float(gfile.get('NABat|Longitude', 0)), 'nabat_site_name': gfile.get('NABat|Site Name', ''), } - + # Extract additional fields with conditionals additional_fields = { - 'nabat_activation_start_time': datetime.strptime(gfile.get('NABat|Activation start time', ''), '%Y%m%dT%H%M%S') if 'NABat|Activation start time' in gfile else None, - 'nabat_activation_end_time': datetime.strptime(gfile.get('NABat|Activation end time', ''), '%Y%m%dT%H%M%S') if 'NABat|Activation end time' in gfile else None, + 'nabat_activation_start_time': datetime.strptime( + gfile.get('NABat|Activation start time', ''), '%Y%m%dT%H%M%S' + ) + if 'NABat|Activation start time' in gfile + else None, + 'nabat_activation_end_time': datetime.strptime( + gfile.get('NABat|Activation end time', ''), '%Y%m%dT%H%M%S' + ) + if 'NABat|Activation end time' in gfile + else None, 'nabat_software_type': gfile.get('NABat|Software type', ''), 'nabat_species_list': gfile.get('NABat|Species List', '').split(','), 'nabat_comments': gfile.get('NABat|Comments', ''), 'nabat_detector_type': gfile.get('NABat|Detector type', ''), 'nabat_unusual_occurrences': gfile.get('NABat|Unusual occurrences', ''), } - + # Combine all extracted fields metadata = {**nabat_fields, **additional_fields} - - + return JsonResponse(metadata, safe=False) - + except Exception as e: return JsonResponse({'error': str(e)}, status=500) diff --git a/scripts/guanoInfo.py b/scripts/guanoInfo.py index ca98dfc..3b45927 100644 --- a/scripts/guanoInfo.py +++ b/scripts/guanoInfo.py @@ -1,7 +1,7 @@ import click -import json import guano + @click.command() @click.argument('input_file', type=click.Path(exists=True)) @click.option('--output', '-o', default='output.json', help='Output JSON file path') @@ -10,18 +10,19 @@ def extract_guano_metadata(input_file, output): # Load a .WAV file with (or without) GUANO metadata g = guano.GuanoFile(input_file) # Get and set metadata values like a Python dict - print(f"GUANO Version: {g['GUANO|Version']}") + print(f"GUANO Version: {g['GUANO|Version']}") # Print all the metadata values - print("All Metadata:") + print('All Metadata:') for key, value in g.items(): - print(f"{key}: {value}") - + print(f'{key}: {value}') + # Write the updated .WAV file back to disk - - click.echo(f"GUANO metadata extracted from {input_file} and saved to {output}") - + + click.echo(f'GUANO metadata extracted from {input_file} and saved to {output}') + except Exception as e: - click.echo(f"Error: {e}") + click.echo(f'Error: {e}') + if __name__ == '__main__': extract_guano_metadata() From f31c47fe9a80a3058b4a23fa58f361c62bb2eac8 Mon Sep 17 00:00:00 2001 From: Bryon Lewis Date: Wed, 3 Apr 2024 11:24:34 -0400 Subject: [PATCH 3/8] client side guano metadata --- bats_ai/core/admin/recording.py | 13 ++ ...ted_species_recording_detector_and_more.py | 48 ++++++ bats_ai/core/models/recording.py | 9 +- bats_ai/core/views/guanometadata.py | 42 ++--- bats_ai/core/views/recording.py | 21 +++ bats_ai/settings.py | 4 + client/src/api/api.ts | 95 ++++++++--- .../src/components/BatchRecordingElement.vue | 112 ++++++++++++- .../src/components/BatchUploadRecording.vue | 80 +++++++-- client/src/components/UploadRecording.vue | 158 ++++++++++++++++-- client/src/use/useUtils.ts | 33 ++++ 11 files changed, 526 insertions(+), 89 deletions(-) create mode 100644 bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py create mode 100644 client/src/use/useUtils.ts diff --git a/bats_ai/core/admin/recording.py b/bats_ai/core/admin/recording.py index b968cf9..8cc217d 100644 --- a/bats_ai/core/admin/recording.py +++ b/bats_ai/core/admin/recording.py @@ -24,6 +24,13 @@ class RecordingAdmin(admin.ModelAdmin): 'recording_location', 'grts_cell_id', 'grts_cell', + 'site_name', + 'detector', + 'software', + 'species_list', + 'unusual_occurrences', + 'get_computed_species', + 'get_official_species', ] list_select_related = True # list_select_related = ['owner'] @@ -34,6 +41,12 @@ class RecordingAdmin(admin.ModelAdmin): autocomplete_fields = ['owner'] readonly_fields = ['created', 'modified'] + def get_official_species(self, instance): + return [species.species_code_6 for species in instance.official_species.all()] + + def get_computed_species(self, instance): + return [species.species_code_6 for species in instance.computed_species.all()] + @admin.display( description='Spectrogram', empty_value='Not computed', diff --git a/bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py b/bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py new file mode 100644 index 0000000..c96c196 --- /dev/null +++ b/bats_ai/core/migrations/0010_recording_computed_species_recording_detector_and_more.py @@ -0,0 +1,48 @@ +# Generated by Django 4.1.13 on 2024-04-03 13:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0009_annotations_type'), + ] + + operations = [ + migrations.AddField( + model_name='recording', + name='computed_species', + field=models.ManyToManyField(related_name='recording_computed_species', to='core.species'), + ), + migrations.AddField( + model_name='recording', + name='detector', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='official_species', + field=models.ManyToManyField(related_name='recording_official_species', to='core.species'), + ), + migrations.AddField( + model_name='recording', + name='site_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='software', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='species_list', + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name='recording', + name='unusual_occurrences', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/bats_ai/core/models/recording.py b/bats_ai/core/models/recording.py index 600687c..6d81036 100644 --- a/bats_ai/core/models/recording.py +++ b/bats_ai/core/models/recording.py @@ -20,10 +20,15 @@ class Recording(TimeStampedModel, models.Model): public = models.BooleanField(default=False) software = models.TextField(blank=True, null=True) detector = models.TextField(blank=True, null=True) - computed_species = models.ManyToManyField(Species) # species from a computed sense + species_list = models.TextField(blank=True, null=True) + site_name = models.TextField(blank=True, null=True) + computed_species = models.ManyToManyField( + Species, related_name='recording_computed_species' + ) # species from a computed sense official_species = models.ManyToManyField( - Species + Species, related_name='recording_official_species' ) # species that are detemrined by the owner or from annotations as official species list + unusual_occurrences = models.TextField(blank=True, null=True) @property def has_spectrogram(self): diff --git a/bats_ai/core/views/guanometadata.py b/bats_ai/core/views/guanometadata.py index 8759214..c113fae 100644 --- a/bats_ai/core/views/guanometadata.py +++ b/bats_ai/core/views/guanometadata.py @@ -1,4 +1,5 @@ from datetime import datetime +import logging from django.http import HttpRequest, JsonResponse from guano import GuanoFile @@ -7,20 +8,21 @@ from ninja.pagination import RouterPaginated router = RouterPaginated() +logger = logging.getLogger(__name__) class GuanoMetadataSchema(Schema): - nabat_grid_cell_grts_id: str - nabat_latitude: float - nabat_longitude: float - nabat_site_name: str - nabat_activation_start_time: datetime - nabat_activation_end_time: datetime - nabat_software_type: str - nabat_species_list: list[str] - nabat_comments: str - nabat_detector_type: str - nabat_unusual_occurrences: str + nabat_grid_cell_grts_id: str | None = None + nabat_latitude: float | None = None + nabat_longitude: float | None = None + nabat_site_name: str | None = None + nabat_activation_start_time: datetime | None = None + nabat_activation_end_time: datetime | None = None + nabat_software_type: str | None = None + nabat_species_list: list[str] | None = None + nabat_comments: str | None = None + nabat_detector_type: str | None = None + nabat_unusual_occurrences: str | None = None router = RouterPaginated() @@ -37,28 +39,28 @@ def default_data( # Extract required NABat fields nabat_fields = { - 'nabat_grid_cell_grts_id': gfile.get('NABat|Grid Cell GRTS ID', ''), - 'nabat_latitude': float(gfile.get('NABat|Latitude', 0)), - 'nabat_longitude': float(gfile.get('NABat|Longitude', 0)), - 'nabat_site_name': gfile.get('NABat|Site Name', ''), + 'nabat_grid_cell_grts_id': gfile.get('NABat|Grid Cell GRTS ID', None), + 'nabat_latitude': (gfile.get('NABat|Latitude', None)), + 'nabat_longitude': (gfile.get('NABat|Longitude', None)), + 'nabat_site_name': gfile.get('NABat|Site Name', None), } # Extract additional fields with conditionals additional_fields = { 'nabat_activation_start_time': datetime.strptime( - gfile.get('NABat|Activation start time', ''), '%Y%m%dT%H%M%S' + gfile.get('NABat|Activation start time', None), '%Y%m%dT%H%M%S' ) if 'NABat|Activation start time' in gfile else None, 'nabat_activation_end_time': datetime.strptime( - gfile.get('NABat|Activation end time', ''), '%Y%m%dT%H%M%S' + gfile.get('NABat|Activation end time', None), '%Y%m%dT%H%M%S' ) if 'NABat|Activation end time' in gfile else None, - 'nabat_software_type': gfile.get('NABat|Software type', ''), + 'nabat_software_type': gfile.get('NABat|Software type', None), 'nabat_species_list': gfile.get('NABat|Species List', '').split(','), - 'nabat_comments': gfile.get('NABat|Comments', ''), - 'nabat_detector_type': gfile.get('NABat|Detector type', ''), + 'nabat_comments': gfile.get('NABat|Comments', None), + 'nabat_detector_type': gfile.get('NABat|Detector type', None), 'nabat_unusual_occurrences': gfile.get('NABat|Unusual occurrences', ''), } diff --git a/bats_ai/core/views/recording.py b/bats_ai/core/views/recording.py index d0ed2fe..e3c5e2d 100644 --- a/bats_ai/core/views/recording.py +++ b/bats_ai/core/views/recording.py @@ -47,6 +47,11 @@ class RecordingUploadSchema(Schema): longitude: float = None gridCellId: int = None publicVal: bool = None + site_name: str = None + software: str = None + detector: str = None + species_list: str = None + unusual_occurrences: str = None class AnnotationSchema(Schema): @@ -109,7 +114,13 @@ def create_recording( recording_location=point, public=publicVal, comments=payload.comments, + detector=payload.detector, + software=payload.software, + site_name=payload.site_name, + species_list=payload.species_list, + unusual_occurrences=payload.unusual_occurrences, ) + recording.save() # Start generating recording as soon as created # this creates the spectrogram during the upload so it is available immediately afterwards @@ -142,6 +153,16 @@ def update_recording(request: HttpRequest, id: int, recording_data: RecordingUpl if recording_data.latitude and recording_data.longitude: point = Point(recording_data.longitude, recording_data.latitude) recording.recording_location = point + if recording_data.detector: + recording.detector = recording_data.detector + if recording_data.software: + recording.software = recording_data.software + if recording_data.site_name: + recording.site_name = recording_data.site_name + if recording_data.species_list: + recording.species_list = recording_data.species_list + if recording_data.unusual_occurrences: + recording.unusual_occurrences = recording_data.unusual_occurrences recording.save() diff --git a/bats_ai/settings.py b/bats_ai/settings.py index 11c29dd..f64c13e 100644 --- a/bats_ai/settings.py +++ b/bats_ai/settings.py @@ -21,6 +21,10 @@ class BatsAiMixin(ConfigMixin): BASE_DIR = Path(__file__).resolve(strict=True).parent.parent + FILE_UPLOAD_HANDLERS = [ + 'django.core.files.uploadhandler.TemporaryFileUploadHandler', + ] + @staticmethod def mutate_configuration(configuration: ComposedConfiguration) -> None: # Install local apps first, to ensure any overridden resources are found first diff --git a/client/src/api/api.ts b/client/src/api/api.ts index 38127c8..e1fcd69 100644 --- a/client/src/api/api.ts +++ b/client/src/api/api.ts @@ -127,55 +127,70 @@ export const axiosInstance = axios.create({ baseURL: import.meta.env.VUE_APP_API_ROOT as string, }); +export interface RecordingFileParameters { + name: string; + recorded_date: string; + recorded_time: string; + equipment: string; + comments: string; + location?: UploadLocation; + publicVal: boolean; + site_name?: string; + software?: string; + detector?: string; + species_list?: string; + unusual_occurrences?: string; + +} -async function uploadRecordingFile(file: File, name: string, recorded_date: string, recorded_time: string, equipment: string, comments: string, publicVal = false, location: UploadLocation = null ) { +async function uploadRecordingFile(file: File, params: RecordingFileParameters ) { const formData = new FormData(); formData.append('audio_file', file); - formData.append('name', name); - formData.append('recorded_date', recorded_date); - formData.append('recorded_time', recorded_time); - formData.append('equipment', equipment); - formData.append('comments', comments); - if (location) { - if (location.latitude && location.longitude) { - formData.append('latitude', location.latitude.toString()); - formData.append('longitude', location.longitude.toString()); + formData.append('name', params.name); + formData.append('recorded_date', params.recorded_date); + formData.append('recorded_time', params.recorded_time); + formData.append('equipment', params.equipment); + formData.append('comments', params.comments); + if (params.location) { + if (params.location.latitude && params.location.longitude) { + formData.append('latitude', params.location.latitude.toString()); + formData.append('longitude', params.location.longitude.toString()); } - if (location.gridCellId) { - formData.append('gridCellId', location.gridCellId.toString()); + if (params.location.gridCellId) { + formData.append('gridCellId', params.location.gridCellId.toString()); } } const recordingParams = { - name, - equipment, - comments, + name: params.name, + equipment: params.equipment, + comments: params.comments, }; const payloadBlob = new Blob([JSON.stringify(recordingParams)], { type: 'application/json' }); formData.append('payload', payloadBlob); await axiosInstance.post('/recording/', formData, { - params: { publicVal }, + params: { publicVal: !!params.publicVal }, headers: { 'Content-Type': 'multipart/form-data', } }); } - async function patchRecording(recordingId: number, name: string, recorded_date: string, recorded_time: string, equipment: string, comments: string, publicVal = false, location: UploadLocation = null ) { - const latitude = location ? location.latitude : undefined; - const longitude = location ? location.longitude : undefined; - const gridCellId = location ? location.gridCellId : undefined; + async function patchRecording(recordingId: number, params: RecordingFileParameters) { + const latitude = params.location ? params.location.latitude : undefined; + const longitude = params.location ? params.location.longitude : undefined; + const gridCellId = params.location ? params.location.gridCellId : undefined; await axiosInstance.patch(`/recording/${recordingId}`, { - name, - recorded_date, - recorded_time, - equipment, - comments, - publicVal, + name: params.name, + recorded_date: params.recorded_date, + recorded_time: params.recorded_time, + equipment: params.equipment, + comments: params.comments, + publicVal: !!params.publicVal, latitude, longitude, gridCellId @@ -271,6 +286,33 @@ async function getCellfromLocation(latitude: number, longitude: number) { return axiosInstance.get(`/grts/grid_cell_id`, {params: {latitude, longitude}}); } +interface GuanoMetadata { + nabat_grid_cell_grts_id?: string + nabat_latitude?: number + nabat_longitude?: number + nabat_site_name?: string + nabat_activation_start_time?: string + nabat_activation_end_time?: string + nabat_software_type?: string + nabat_species_list?: string[] + nabat_comments?: string + nabat_detector_type?: string + nabat_unusual_occurrences?: string + +} + +async function getGuanoMetadata(file: File): Promise { + const formData = new FormData(); + formData.append('audio_file', file); + const results = await axiosInstance.post('/guano/',formData, { + headers: { + 'Content-Type': 'multipart/form-data', + } + }); + return results.data; + +} + export { uploadRecordingFile, getRecordings, @@ -291,4 +333,5 @@ export { deleteTemporalAnnotation, getCellLocation, getCellfromLocation, + getGuanoMetadata, }; \ No newline at end of file diff --git a/client/src/components/BatchRecordingElement.vue b/client/src/components/BatchRecordingElement.vue index 1d4c047..ab5d7ac 100644 --- a/client/src/components/BatchRecordingElement.vue +++ b/client/src/components/BatchRecordingElement.vue @@ -1,9 +1,10 @@ diff --git a/client/src/components/RecordingInfoDisplay.vue b/client/src/components/RecordingInfoDisplay.vue new file mode 100644 index 0000000..d5ba999 --- /dev/null +++ b/client/src/components/RecordingInfoDisplay.vue @@ -0,0 +1,96 @@ + + + diff --git a/client/src/views/Recordings.vue b/client/src/views/Recordings.vue index a71b5c7..42394e2 100644 --- a/client/src/views/Recordings.vue +++ b/client/src/views/Recordings.vue @@ -5,11 +5,13 @@ import UploadRecording, { EditingRecording } from '../components/UploadRecording import MapLocation from '../components/MapLocation.vue'; import useState from '../use/useState'; import BatchUploadRecording from '../components/BatchUploadRecording.vue'; +import RecordingInfoDisplay from '../components/RecordingInfoDisplay.vue'; export default defineComponent({ components: { UploadRecording, MapLocation, BatchUploadRecording, + RecordingInfoDisplay }, setup() { const itemsPerPage = ref(-1); @@ -55,12 +57,8 @@ export default defineComponent({ key:'recording_location' }, { - title:'Equipment', - key:'equipment', - }, - { - title:'Comments', - key:'comments', + title: 'Details', + key:'comments' }, { title:'Users Annotated', @@ -95,16 +93,13 @@ export default defineComponent({ }, { title: 'Location', - key:'recording_location' + key:'details' }, { - title:'Equipment', - key:'equipment', - }, - { - title:'Comments', - key:'comments', + title: 'Details', + key:'comments' }, + { title:'Annotated by Me', key:'userMadeAnnotations', @@ -278,6 +273,24 @@ export default defineComponent({ + + + +