From 0972bd2c42314d4f2ef1947c07eb464380c294be Mon Sep 17 00:00:00 2001 From: Eduardo Rosendo Date: Wed, 26 Jun 2024 16:29:15 -0400 Subject: [PATCH 1/4] feat(tasks): Adds helper method to convert audios to the ogg format --- doctor/tasks.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/doctor/tasks.py b/doctor/tasks.py index 1077925..f3f6aea 100644 --- a/doctor/tasks.py +++ b/doctor/tasks.py @@ -479,6 +479,47 @@ def convert_to_mp3(output_path: AnyStr, media: Any) -> None: return output_path +def convert_to_ogg(output_path: AnyStr, media: Any) -> None: + """Converts audio data to the ogg format (.ogg) + + This function uses ffmpeg to convert the audio data provided in `media` to + the ogg format with the following specifications: + + * Single audio channel (`-ac 1`) + * 8 kHz sampling rate (`-b:a 8k`) + * Optimized for voice over IP applications (`-application voip`) + + :param output_path: Audio file bytes sent to Doctor + :param media: Temporary filepath for output of audioprocess + :return: + """ + av_command = [ + "ffmpeg", + "-i", + "/dev/stdin", + "-vn", + "-map_metadata", + "-1", + "-ac", + "1", + "-c:a", + "libopus", + "-b:a", + "8k", + "-application", + "voip", + "-f", + "ogg", + output_path, + ] + + ffmpeg_cmd = subprocess.Popen( + av_command, stdin=subprocess.PIPE, stdout=subprocess.PIPE, shell=False + ) + ffmpeg_cmd.communicate(media.read()) + return output_path + + def set_mp3_meta_data( audio_data: Dict, mp3_path: AnyStr ) -> eyed3.core.AudioFile: From 0838ffc0ba41c1aa6d11d6795876aede22d330e4 Mon Sep 17 00:00:00 2001 From: Eduardo Rosendo Date: Wed, 26 Jun 2024 16:30:37 -0400 Subject: [PATCH 2/4] feat(urls): Update audio endpoint to support OGG conversion --- doctor/urls.py | 4 ++-- doctor/views.py | 16 ++++++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/doctor/urls.py b/doctor/urls.py index e151bc6..59a90bb 100644 --- a/doctor/urls.py +++ b/doctor/urls.py @@ -1,4 +1,4 @@ -from django.urls import path +from django.urls import path, re_path from . import views @@ -23,7 +23,7 @@ views.make_png_thumbnails_from_range, name="thumbnails", ), - path("convert/audio/mp3/", views.convert_audio, name="convert-audio"), + re_path("convert/audio/(mp3|ogg)/", views.convert_audio, name="convert-audio"), path("utils/page-count/pdf/", views.page_count, name="page_count"), path("utils/mime-type/", views.extract_mime_type, name="mime_type"), path( diff --git a/doctor/views.py b/doctor/views.py index 16b1de6..4ed98f1 100644 --- a/doctor/views.py +++ b/doctor/views.py @@ -34,6 +34,7 @@ from doctor.tasks import ( convert_tiff_to_pdf_bytes, convert_to_mp3, + convert_to_ogg, download_images, extract_from_doc, extract_from_docx, @@ -367,8 +368,9 @@ def fetch_audio_duration(request) -> HttpResponse: return HttpResponse(str(e)) -def convert_audio(request) -> Union[FileResponse, HttpResponse]: - """Convert audio file to MP3 and update metadata on mp3. +def convert_audio(request, output_format: str) -> Union[FileResponse, HttpResponse]: + """Converts an uploaded audio file to the specified output format and + updates its metadata. :return: Converted audio """ @@ -378,8 +380,14 @@ def convert_audio(request) -> Union[FileResponse, HttpResponse]: filepath = form.cleaned_data["fp"] media_file = form.cleaned_data["file"] audio_data = {k: v[0] for k, v in dict(request.GET).items()} - convert_to_mp3(filepath, media_file) - set_mp3_meta_data(audio_data, filepath) + match output_format: + case 'mp3': + convert_to_mp3(filepath, media_file) + set_mp3_meta_data(audio_data, filepath) + case 'ogg': + convert_to_ogg(filepath, media_file) + case _: + raise NotImplemented response = FileResponse(open(filepath, "rb")) cleanup_form(form) return response From 36b78ae0640f964e219c40da2087f4569ea6c359 Mon Sep 17 00:00:00 2001 From: Eduardo Rosendo Date: Wed, 26 Jun 2024 16:33:09 -0400 Subject: [PATCH 3/4] feat(forms): Allow flexible output format by removing extension from fp --- doctor/forms.py | 2 +- doctor/urls.py | 4 +++- doctor/views.py | 8 +++++--- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/doctor/forms.py b/doctor/forms.py index 04c229e..f01cfce 100644 --- a/doctor/forms.py +++ b/doctor/forms.py @@ -44,7 +44,7 @@ class AudioForm(BaseAudioFile): audio_data = forms.JSONField(label="audio-data", required=False) def clean(self): - self.cleaned_data["fp"] = f"/tmp/audio_{uuid.uuid4().hex}.mp3" + self.cleaned_data["fp"] = f"/tmp/audio_{uuid.uuid4().hex}" if self.cleaned_data.get("file", None): filename = self.cleaned_data["file"].name self.cleaned_data["extension"] = filename.split(".")[-1] diff --git a/doctor/urls.py b/doctor/urls.py index 59a90bb..8220bed 100644 --- a/doctor/urls.py +++ b/doctor/urls.py @@ -23,7 +23,9 @@ views.make_png_thumbnails_from_range, name="thumbnails", ), - re_path("convert/audio/(mp3|ogg)/", views.convert_audio, name="convert-audio"), + re_path( + "convert/audio/(mp3|ogg)/", views.convert_audio, name="convert-audio" + ), path("utils/page-count/pdf/", views.page_count, name="page_count"), path("utils/mime-type/", views.extract_mime_type, name="mime_type"), path( diff --git a/doctor/views.py b/doctor/views.py index 4ed98f1..795248d 100644 --- a/doctor/views.py +++ b/doctor/views.py @@ -368,7 +368,9 @@ def fetch_audio_duration(request) -> HttpResponse: return HttpResponse(str(e)) -def convert_audio(request, output_format: str) -> Union[FileResponse, HttpResponse]: +def convert_audio( + request, output_format: str +) -> Union[FileResponse, HttpResponse]: """Converts an uploaded audio file to the specified output format and updates its metadata. @@ -381,10 +383,10 @@ def convert_audio(request, output_format: str) -> Union[FileResponse, HttpRespon media_file = form.cleaned_data["file"] audio_data = {k: v[0] for k, v in dict(request.GET).items()} match output_format: - case 'mp3': + case "mp3": convert_to_mp3(filepath, media_file) set_mp3_meta_data(audio_data, filepath) - case 'ogg': + case "ogg": convert_to_ogg(filepath, media_file) case _: raise NotImplemented From 2114dc8d51b47801e6290b8e227641ae7f082985 Mon Sep 17 00:00:00 2001 From: Eduardo Rosendo Date: Wed, 26 Jun 2024 18:51:21 -0400 Subject: [PATCH 4/4] feat(docs): Update the readme file --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c5da289..0c88907 100644 --- a/README.md +++ b/README.md @@ -311,14 +311,25 @@ from courts across the country and standardizes the format for our end users. This endpoint also adds the SEAL of the court to the MP3 file and updates the metadata to reflect our updates. -This isn't the cleanest of CURLs because we have to convert the large JSON file to a query string, but for proof of concept here is the result - curl 'http://localhost:5050/convert/audio/mp3/?audio_data=%7B%22court_full_name%22%3A+%22Testing+Supreme+Court%22%2C+%22court_short_name%22%3A+%22Testing+Supreme+Court%22%2C+%22court_pk%22%3A+%22test%22%2C+%22court_url%22%3A+%22http%3A%2F%2Fwww.example.com%2F%22%2C+%22docket_number%22%3A+%22docket+number+1+005%22%2C+%22date_argued%22%3A+%222020-01-01%22%2C+%22date_argued_year%22%3A+%222020%22%2C+%22case_name%22%3A+%22SEC+v.+Frank+J.+Custable%2C+Jr.%22%2C+%22case_name_full%22%3A+%22case+name+full%22%2C+%22case_name_short%22%3A+%22short%22%2C+%22download_url%22%3A+%22http%3A%2F%2Fmedia.ca7.uscourts.gov%2Fsound%2Fexternal%2Fgw.15-1442.15-1442_07_08_2015.mp3%22%7D' \ -X 'POST' \ -F "file=@doctor/test_assets/1.wma" This returns the audio file as a file response. +### Endpoint: /convert/audio/ogg/ + +This endpoint takes an audio file and converts it to an OGG file. The conversion process downsizes files by using +a single audio channel and fixing the sampling rate to 8 kHz. + +This endpoint also optimizes the output for voice over IP applications. + + curl 'http://localhost:5050/convert/audio/ogg/' \ + -X 'POST' \ + -F "file=@doctor/test_assets/1.wma" + +This returns the audio file as a file response. + ## Testing