diff --git a/app/api/tests/test_utils.py b/app/api/tests/test_utils.py index e12a0917..d0ae78ca 100644 --- a/app/api/tests/test_utils.py +++ b/app/api/tests/test_utils.py @@ -4,9 +4,10 @@ from django.test import TestCase from seqeval.metrics.sequence_labeling import get_entities +from ..exceptions import FileParseException from ..models import Label, Document from ..utils import BaseStorage, ClassificationStorage, SequenceLabelingStorage, Seq2seqStorage, CoNLLParser -from ..utils import iterable_to_io +from ..utils import AudioParser, iterable_to_io class TestBaseStorage(TestCase): @@ -138,6 +139,26 @@ class TestCoNLLParser(TestCase): }) +class TestAudioParser(TestCase): + def test_parse_mp3(self): + f = io.BytesIO(b'...') + f.name = 'test.mp3' + + actual = next(AudioParser().parse(f)) + + self.assertEqual(actual, [{ + 'audio': 'data:audio/mpeg;base64,Li4u', + 'meta': '{"filename": "test.mp3"}', + }]) + + def test_parse_unknown(self): + f = io.BytesIO(b'...') + f.name = 'unknown.unknown' + + with self.assertRaises(FileParseException): + next(AudioParser().parse(f)) + + class TestIterableToIO(TestCase): def test(self): def iterable(): diff --git a/app/api/utils.py b/app/api/utils.py index ae5e3510..a2c6ee71 100644 --- a/app/api/utils.py +++ b/app/api/utils.py @@ -1,7 +1,9 @@ +import base64 import csv import io import itertools import json +import mimetypes import re from collections import defaultdict @@ -227,7 +229,9 @@ class Speech2textStorage(BaseStorage): @transaction.atomic def save(self, user): for data in self.data: - doc = self.save_doc([{'text': audio['audio']} for audio in data]) + for audio in data: + audio['text'] = audio.pop('audio') + doc = self.save_doc(data) annotations = self.make_annotations(doc, data) self.save_annotation(annotations, user) @@ -411,6 +415,19 @@ class JSONParser(FileParser): yield data +class AudioParser(FileParser): + def parse(self, file): + file_type, _ = mimetypes.guess_type(file.name, strict=False) + if not file_type: + raise FileParseException(line_num=1, line='Unable to guess file type') + + audio = base64.b64encode(file.read()) + yield [{ + 'audio': f'data:{file_type};base64,{audio.decode("ascii")}', + 'meta': json.dumps({'filename': file.name}), + }] + + class JSONLRenderer(JSONRenderer): def render(self, data, accepted_media_type=None, renderer_context=None): diff --git a/app/api/views.py b/app/api/views.py index 4b31bcd8..8e81295d 100644 --- a/app/api/views.py +++ b/app/api/views.py @@ -18,7 +18,7 @@ from .models import Project, Label, Document, RoleMapping, Role from .permissions import IsProjectAdmin, IsAnnotatorAndReadOnly, IsAnnotator, IsAnnotationApproverAndReadOnly, IsOwnAnnotation, IsAnnotationApprover from .serializers import ProjectSerializer, LabelSerializer, DocumentSerializer, UserSerializer from .serializers import ProjectPolymorphicSerializer, RoleMappingSerializer, RoleSerializer -from .utils import CSVParser, ExcelParser, JSONParser, PlainTextParser, CoNLLParser, iterable_to_io +from .utils import CSVParser, ExcelParser, JSONParser, PlainTextParser, CoNLLParser, AudioParser, iterable_to_io from .utils import JSONLRenderer from .utils import JSONPainter, CSVPainter @@ -243,6 +243,8 @@ class TextUploadAPI(APIView): return CoNLLParser() elif file_format == 'excel': return ExcelParser() + elif file_format == 'audio': + return AudioParser() else: raise ValidationError('format {} is invalid.'.format(file_format)) diff --git a/app/server/static/components/annotation.pug b/app/server/static/components/annotation.pug index a3aaacf8..510930ed 100644 --- a/app/server/static/components/annotation.pug +++ b/app/server/static/components/annotation.pug @@ -74,7 +74,9 @@ div.columns(v-cloak="") ) span.icon i.fa.fa-check(v-show="annotations[index] && annotations[index].length") - span.name {{ doc.text.slice(0, 60) }}... + span.name + span(v-if="documentMetadataFor(index) && documentMetadataFor(index).filename") {{ documentMetadataFor(index).filename }} + span(v-else) {{ doc.text.slice(0, 60) }}... div.column.is-7.is-offset-1.message.hero.is-fullheight#message-pane diff --git a/app/server/static/components/annotationMixin.js b/app/server/static/components/annotationMixin.js index b1c1d2dd..7569f3c1 100644 --- a/app/server/static/components/annotationMixin.js +++ b/app/server/static/components/annotationMixin.js @@ -177,6 +177,20 @@ export default { }); }, + documentMetadataFor(i) { + const document = this.docs[i]; + if (document == null || document.meta == null) { + return null; + } + + const metadata = JSON.parse(document.meta); + if (isEmpty(metadata)) { + return null; + } + + return metadata; + }, + getState() { if (this.picked === 'all') { return ''; @@ -292,17 +306,7 @@ export default { }, documentMetadata() { - const document = this.docs[this.pageNumber]; - if (document == null || document.meta == null) { - return null; - } - - const metadata = JSON.parse(document.meta); - if (isEmpty(metadata)) { - return null; - } - - return metadata; + return this.documentMetadataFor(this.pageNumber); }, id2label() { diff --git a/app/server/static/components/examples/upload_speech2text.jsonl b/app/server/static/components/examples/upload_speech2text.jsonl index 300ae3b9..10bb051b 100644 --- a/app/server/static/components/examples/upload_speech2text.jsonl +++ b/app/server/static/components/examples/upload_speech2text.jsonl @@ -1,3 +1,3 @@ -{"audio": "data:audio/mpeg;base64,..."} +{"audio": "data:audio/mpeg;base64,...", "meta":{"filename": "a-story.mp3"}} {"audio": "https://server.com/audio.ogg"} {"audio": "data:audio/wav;base64,...", "transcription": "Once upon a time..."} diff --git a/app/server/static/components/upload_speech2text.vue b/app/server/static/components/upload_speech2text.vue index 9d6ef29a..a70b928d 100644 --- a/app/server/static/components/upload_speech2text.vue +++ b/app/server/static/components/upload_speech2text.vue @@ -12,6 +12,16 @@ block select-format-area ) | JSONL + label.radio + input( + type="radio" + name="format" + value="audio" + v-bind:checked="format === 'audio'" + v-model="format" + ) + | Audio + block example-format-area pre.code-block(v-show="format === 'json'") code.json diff --git a/app/server/templates/dataset.html b/app/server/templates/dataset.html index 06c11bc5..b74df896 100644 --- a/app/server/templates/dataset.html +++ b/app/server/templates/dataset.html @@ -24,7 +24,7 @@ {% for doc in object_list %} {{ forloop.counter0|add:page_obj.start_index }} - {{ doc.text|truncatechars:200 }} + {{ doc.text|truncatechars:100 }}