From 9a53c3c0ae6a4eafd39aa3d9713f8bb0c577881f Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Fri, 26 Jul 2024 10:32:11 +0200 Subject: [PATCH 01/81] Add `config.ini` and `.env` files to the `datas` variable of `audiotext.spec` to make it compatible with the latest package version (6.9.0) --- audiotext.spec | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/audiotext.spec b/audiotext.spec index e95c402..25e8705 100644 --- a/audiotext.spec +++ b/audiotext.spec @@ -14,7 +14,9 @@ datas = [ (r'venv/Lib/site-packages/pyannote', 'pyannote'), (r'venv/Lib/site-packages/asteroid_filterbanks', 'asteroid_filterbanks'), (r'venv/Lib/site-packages/whisperx', 'whisperx'), - ('res', 'res') + ('res', 'res'), + ('config.ini', '.'), + ('.env', '.'), ] datas += copy_metadata('torch') @@ -122,6 +124,3 @@ else: upx_exclude=[], name='audiotext', ) - -copyfile('config.ini', '{0}/audiotext/config.ini'.format(DISTPATH)) -copyfile('.env', '{0}/audiotext/.env'.format(DISTPATH)) From 6aee0efda10bcbf64b027467f28f3cec1db76018 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Fri, 26 Jul 2024 10:37:07 +0200 Subject: [PATCH 02/81] Add missing documentation about Whisper API and the`.env` file to the `README` --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47d12b8..0b07d13 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ ![Main](docs/main-system.png) -**Audiotext** transcribes the audio from an audio file, video file, microphone input, directory, or YouTube video into one of the 99 different languages it supports. You can transcribe using the [**Google Speech-to-Text API**](https://cloud.google.com/speech-to-text) or [**WhisperX**](https://github.com/m-bain/whisperX), which can even translate the transcription or generate subtitles! +**Audiotext** transcribes the audio from an audio file, video file, microphone input, directory, or YouTube video into any of the 99 different languages it supports. You can transcribe using the [**Google Speech-to-Text API**](https://cloud.google.com/speech-to-text), the [**Whisper API**](https://platform.openai.com/docs/guides/speech-to-text), or [**WhisperX**](https://github.com/m-bain/whisperX). The last two methods can even translate the transcription or generate subtitles! You can also choose the theme you like best. It can be dark, light, or the one configured in the system. @@ -479,7 +479,8 @@ You can also choose the theme you like best. It can be dark, light, or the one c source venv/Scripts/activate ``` 5. Run `pip install -r requirements.txt` to install the dependencies. -6. Run `python src/app.py` to start the program. +6. Copy and paste the `.env.example` file as `.env` to the root of the directory. +7. Run `python src/app.py` to start the program. ### Notes - You cannot generate a single executable file for this project with PyInstaller due to the dependency with the CustomTkinter package (reason [here](https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging)). From 928dd88d23ae581bc880295b9096637045f28321 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 13:39:03 +0200 Subject: [PATCH 03/81] Add missing __init__ files --- src/handlers/__init__.py | 0 src/interfaces/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/handlers/__init__.py create mode 100644 src/interfaces/__init__.py diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/interfaces/__init__.py b/src/interfaces/__init__.py new file mode 100644 index 0000000..e69de29 From 2210a02343084bc0a580022611a0255190eeacf4 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 13:47:23 +0200 Subject: [PATCH 04/81] Add the `mypy` package to the `requirements-dev.txt` file --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..776bb0d --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1 @@ +mypy==1.11.0 From d843b8513339f96936645b60684f162942dea583 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 14:03:10 +0200 Subject: [PATCH 05/81] Add the `pyproject.toml` file to disable the `import-untyped` error due to untyped third-party libraries --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..07b42f4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.mypy] +disable_error_code = "import-untyped" From eeca1e3dc6c57c2780a8cc2e14cd268761e6b033 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 14:14:41 +0200 Subject: [PATCH 06/81] Add typing and documentation to `find_key_by_value` --- src/utils/dict_utils.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/utils/dict_utils.py b/src/utils/dict_utils.py index e975902..4fd3ffb 100644 --- a/src/utils/dict_utils.py +++ b/src/utils/dict_utils.py @@ -1,5 +1,19 @@ -def find_key_by_value(dictionary, target_value): +from typing import Any, Optional + + +def find_key_by_value(dictionary: dict[Any, Any], target_value: Any) -> Optional[Any]: + """ + Searches for the first key in the dictionary that has the specified target value. + + :param dictionary: The dictionary to search through. + :type dictionary: Dict[Any, Any] + :param target_value: The value to search for. + :type target_value: Any + :return: The key associated with the target value, or None if not found. + :rtype: Optional[Any] + """ for key, value in dictionary.items(): if value == target_value: return key + return None From d26e9889c2a22612489340b55387891401e65b31 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 14:33:21 +0200 Subject: [PATCH 07/81] Add missing return type annotation to `save_audio_data` --- src/utils/audio_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils/audio_utils.py b/src/utils/audio_utils.py index 8a4b4ce..e9dd4e9 100644 --- a/src/utils/audio_utils.py +++ b/src/utils/audio_utils.py @@ -2,7 +2,7 @@ from pydub import AudioSegment -def save_audio_data(audio_data: list[sr.AudioData], filename: str): +def save_audio_data(audio_data: list[sr.AudioData], filename: str) -> None: """ Save recorded audio data to a WAV file. @@ -10,6 +10,8 @@ def save_audio_data(audio_data: list[sr.AudioData], filename: str): :type audio_data: list[sr.AudioData] :param filename: The name of the file to save the audio data to. :type filename: str + :return: None + :rtype: None """ if audio_data: raw_audio_data = b"".join( From fd15f0087c17c940d46fc181793af499f95288b6 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 15:35:20 +0200 Subject: [PATCH 08/81] Parse the return type of the `value_type` methods to string and add missing documentation of the method --- src/models/config/config_subtitles.py | 9 +++++++-- src/models/config/config_system.py | 9 +++++++-- src/models/config/config_transcription.py | 9 +++++++-- src/models/config/config_whisper_api.py | 4 ++-- src/models/config/config_whisperx.py | 4 ++-- 5 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/models/config/config_subtitles.py b/src/models/config/config_subtitles.py index 88af1c9..ce2ef6c 100644 --- a/src/models/config/config_subtitles.py +++ b/src/models/config/config_subtitles.py @@ -20,11 +20,16 @@ class Key(Enum): MAX_LINE_WIDTH = "max_line_width" def value_type(self) -> Optional[str]: - """Get the value type associated with the ConfigKey.""" + """ + Get the value type associated with the ConfigKey. + + :return: The type of the value as a string, or None if the key is not found. + :rtype: str + """ type_mapping = { self.HIGHLIGHT_WORDS: "bool", self.MAX_LINE_COUNT: "int", self.MAX_LINE_WIDTH: "int", } - return type_mapping.get(self, None) + return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_system.py b/src/models/config/config_system.py index 35202c9..b5e0a66 100644 --- a/src/models/config/config_system.py +++ b/src/models/config/config_system.py @@ -16,7 +16,12 @@ class Key(Enum): APPEARANCE_MODE = "appearance_mode" def value_type(self) -> Optional[str]: - """Get the value type associated with the ConfigKey.""" + """ + Get the value type associated with the ConfigKey. + + :return: The type of the value as a string, or None if the key is not found. + :rtype: str + """ type_mapping = {self.APPEARANCE_MODE: "str"} - return type_mapping.get(self, None) + return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_transcription.py b/src/models/config/config_transcription.py index 7239f44..983ea12 100644 --- a/src/models/config/config_transcription.py +++ b/src/models/config/config_transcription.py @@ -24,7 +24,12 @@ class Key(Enum): OVERWRITE_FILES = "overwrite_files" def value_type(self) -> Optional[str]: - """Get the value type associated with the ConfigKey.""" + """ + Get the value type associated with the ConfigKey. + + :return: The type of the value as a string, or None if the key is not found. + :rtype: str + """ type_mapping = { self.LANGUAGE: "str", self.AUDIO_SOURCE: "str", @@ -33,4 +38,4 @@ def value_type(self) -> Optional[str]: self.OVERWRITE_FILES: "bool", } - return type_mapping.get(self, None) + return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_whisper_api.py b/src/models/config/config_whisper_api.py index e20032c..7ef1c0e 100644 --- a/src/models/config/config_whisper_api.py +++ b/src/models/config/config_whisper_api.py @@ -23,7 +23,7 @@ def value_type(self) -> Optional[str]: """ Get the value type associated with the ConfigKey. - :return + :return: The type of the value as a string, or None if the key is not found. :rtype: str """ type_mapping = { @@ -32,4 +32,4 @@ def value_type(self) -> Optional[str]: self.TIMESTAMP_GRANULARITIES: "str", } - return type_mapping.get(self, None) + return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_whisperx.py b/src/models/config/config_whisperx.py index eecbcfe..61e3146 100644 --- a/src/models/config/config_whisperx.py +++ b/src/models/config/config_whisperx.py @@ -29,7 +29,7 @@ def value_type(self) -> Optional[str]: """ Get the value type associated with the ConfigKey. - :return + :return: The type of the value as a string, or None if the key is not found. :rtype: str """ type_mapping = { @@ -41,4 +41,4 @@ def value_type(self) -> Optional[str]: self.OUTPUT_FILE_TYPES: "str", } - return type_mapping.get(self, None) + return str(type_mapping.get(self, None)) From ae0f8764a54db431948e7d1554d30de9fde6ee32 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 15:59:13 +0200 Subject: [PATCH 09/81] Fix `No overload variant of get of dict matches argument types Key, None` error in `value_type` methods --- src/models/config/config_subtitles.py | 6 +++--- src/models/config/config_system.py | 2 +- src/models/config/config_transcription.py | 10 +++++----- src/models/config/config_whisper_api.py | 6 +++--- src/models/config/config_whisperx.py | 12 ++++++------ 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/models/config/config_subtitles.py b/src/models/config/config_subtitles.py index ce2ef6c..008a1a5 100644 --- a/src/models/config/config_subtitles.py +++ b/src/models/config/config_subtitles.py @@ -27,9 +27,9 @@ def value_type(self) -> Optional[str]: :rtype: str """ type_mapping = { - self.HIGHLIGHT_WORDS: "bool", - self.MAX_LINE_COUNT: "int", - self.MAX_LINE_WIDTH: "int", + ConfigSubtitles.Key.HIGHLIGHT_WORDS: "bool", + ConfigSubtitles.Key.MAX_LINE_COUNT: "int", + ConfigSubtitles.Key.MAX_LINE_WIDTH: "int", } return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_system.py b/src/models/config/config_system.py index b5e0a66..417d027 100644 --- a/src/models/config/config_system.py +++ b/src/models/config/config_system.py @@ -22,6 +22,6 @@ def value_type(self) -> Optional[str]: :return: The type of the value as a string, or None if the key is not found. :rtype: str """ - type_mapping = {self.APPEARANCE_MODE: "str"} + type_mapping = {ConfigSystem.Key.APPEARANCE_MODE: "str"} return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_transcription.py b/src/models/config/config_transcription.py index 983ea12..82dee7f 100644 --- a/src/models/config/config_transcription.py +++ b/src/models/config/config_transcription.py @@ -31,11 +31,11 @@ def value_type(self) -> Optional[str]: :rtype: str """ type_mapping = { - self.LANGUAGE: "str", - self.AUDIO_SOURCE: "str", - self.METHOD: "str", - self.AUTOSAVE: "bool", - self.OVERWRITE_FILES: "bool", + ConfigTranscription.Key.LANGUAGE: "str", + ConfigTranscription.Key.AUDIO_SOURCE: "str", + ConfigTranscription.Key.METHOD: "str", + ConfigTranscription.Key.AUTOSAVE: "bool", + ConfigTranscription.Key.OVERWRITE_FILES: "bool", } return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_whisper_api.py b/src/models/config/config_whisper_api.py index 7ef1c0e..36d3e7f 100644 --- a/src/models/config/config_whisper_api.py +++ b/src/models/config/config_whisper_api.py @@ -27,9 +27,9 @@ def value_type(self) -> Optional[str]: :rtype: str """ type_mapping = { - self.RESPONSE_FORMAT: "str", - self.TEMPERATURE: "float", - self.TIMESTAMP_GRANULARITIES: "str", + ConfigWhisperApi.Key.RESPONSE_FORMAT: "str", + ConfigWhisperApi.Key.TEMPERATURE: "float", + ConfigWhisperApi.Key.TIMESTAMP_GRANULARITIES: "str", } return str(type_mapping.get(self, None)) diff --git a/src/models/config/config_whisperx.py b/src/models/config/config_whisperx.py index 61e3146..0deb2d4 100644 --- a/src/models/config/config_whisperx.py +++ b/src/models/config/config_whisperx.py @@ -33,12 +33,12 @@ def value_type(self) -> Optional[str]: :rtype: str """ type_mapping = { - self.MODEL_SIZE: "str", - self.BATCH_SIZE: "int", - self.COMPUTE_TYPE: "str", - self.USE_CPU: "bool", - self.CAN_USE_GPU: "bool", - self.OUTPUT_FILE_TYPES: "str", + ConfigWhisperX.Key.MODEL_SIZE: "str", + ConfigWhisperX.Key.BATCH_SIZE: "int", + ConfigWhisperX.Key.COMPUTE_TYPE: "str", + ConfigWhisperX.Key.USE_CPU: "bool", + ConfigWhisperX.Key.CAN_USE_GPU: "bool", + ConfigWhisperX.Key.OUTPUT_FILE_TYPES: "str", } return str(type_mapping.get(self, None)) From 1b56eb78098d50d9ba4765a3112c57d702142e4e Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 16:02:08 +0200 Subject: [PATCH 10/81] Add missing type parameters for generic type `tuple` --- src/views/custom_widgets/ctk_input_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/custom_widgets/ctk_input_dialog.py b/src/views/custom_widgets/ctk_input_dialog.py index 132ed4e..52ec5cc 100644 --- a/src/views/custom_widgets/ctk_input_dialog.py +++ b/src/views/custom_widgets/ctk_input_dialog.py @@ -4,7 +4,7 @@ from utils.enums import Color -class CTkInputDialog(ctk.CTkToplevel): +class CTkInputDialog(ctk.CTkToplevel): # type: ignore """ Dialog with extra window, message, entry widget, cancel and ok button. For detailed information check out the documentation. @@ -21,7 +21,7 @@ def __init__( entry_border_color: Optional[Union[str, Tuple[str, str]]] = None, entry_text_color: Optional[Union[str, Tuple[str, str]]] = None, title: str = "CTkDialog", - font: Optional[Union[tuple, ctk.CTkFont]] = None, + font: Optional[Union[Tuple[int, str], ctk.CTkFont]] = None, label_text: str = "CTkDialog", entry_text: Optional[str] = None, ): From f6ec434868ef615211c6101f85a6641036d08aa1 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 16:06:07 +0200 Subject: [PATCH 11/81] Add missing return type annotations to `CTkInputDialog` --- src/views/custom_widgets/ctk_input_dialog.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/views/custom_widgets/ctk_input_dialog.py b/src/views/custom_widgets/ctk_input_dialog.py index 52ec5cc..b45020a 100644 --- a/src/views/custom_widgets/ctk_input_dialog.py +++ b/src/views/custom_widgets/ctk_input_dialog.py @@ -85,7 +85,7 @@ def __init__( self.resizable(False, False) self.grab_set() # make other windows not clickable - def _create_widgets(self): + def _create_widgets(self) -> None: self.grid_columnconfigure((0, 1), weight=1) self.rowconfigure(0, weight=1) @@ -150,19 +150,19 @@ def _create_widgets(self): self.after(150, lambda: self._entry.focus()) self._entry.bind("", self._ok_event) - def _ok_event(self): + def _ok_event(self) -> None: self._user_input = self._entry.get() self.grab_release() self.destroy() - def _on_closing(self): + def _on_closing(self) -> None: self.grab_release() self.destroy() - def _cancel_event(self): + def _cancel_event(self) -> None: self.grab_release() self.destroy() - def get_input(self): + def get_input(self) -> Union[str, None]: self.master.wait_window(self) return self._user_input From 98c3639c093c2e87509a35e1e0fb13c64b6677ee Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 16:11:54 +0200 Subject: [PATCH 12/81] Update `CTkScrollableDropdown` --- .../ctk_scrollable_dropdown/__init__.py | 2 +- .../ctk_scrollable_dropdown.py | 45 +++++++++++++------ .../ctk_scrollable_dropdown_frame.py | 24 +++++++--- 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py b/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py index eb90a08..370a977 100644 --- a/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py +++ b/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py @@ -6,7 +6,7 @@ Homepage: https://github.com/Akascape/CTkScrollableDropdown """ -__version__ = "1.0" +__version__ = "1.2" from .ctk_scrollable_dropdown import CTkScrollableDropdown from .ctk_scrollable_dropdown_frame import CTkScrollableDropdownFrame diff --git a/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown.py b/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown.py index c5acf97..0756a5f 100644 --- a/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown.py +++ b/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown.py @@ -37,9 +37,9 @@ def __init__( text_color=None, autocomplete=False, hover_color=None, - **button_kwargs + **button_kwargs, ): - super().__init__(takefocus=1) + super().__init__(master=attach.winfo_toplevel(), takefocus=1) self.focus() self.lift() @@ -83,6 +83,11 @@ def __init__( lambda e: self._withdraw() if not self.disable else None, add="+", ) + self.bind( + "", + lambda e: self._withdraw() if not self.disable else None, + add="+", + ) self.attributes("-alpha", 0) self.disable = False @@ -173,14 +178,14 @@ def __init__( # Add binding for different ctk widgets if ( double_click - or self.attach.winfo_name().startswith("!ctkentry") - or self.attach.winfo_name().startswith("!ctkcombobox") + or type(self.attach) is customtkinter.CTkEntry + or type(self.attach) is customtkinter.CTkComboBox ): self.attach.bind("", lambda e: self._iconify(), add="+") else: self.attach.bind("", lambda e: self._iconify(), add="+") - if self.attach.winfo_name().startswith("!ctkcombobox"): + if type(self.attach) is customtkinter.CTkComboBox: self.attach._canvas.tag_bind( "right_parts", "", lambda e: self._iconify() ) @@ -190,7 +195,7 @@ def __init__( if self.command is None: self.command = self.attach.set - if self.attach.winfo_name().startswith("!ctkoptionmenu"): + if type(self.attach) is customtkinter.CTkOptionMenu: self.attach._canvas.bind("", lambda e: self._iconify()) self.attach._text_label.bind("", lambda e: self._iconify()) if self.command is None: @@ -205,7 +210,6 @@ def __init__( if self.autocomplete: self.bind_autocomplete() - self.deiconify() self.withdraw() self.attributes("-alpha", self.alpha) @@ -214,6 +218,8 @@ def _destroy(self): self.after(500, self.destroy_popup) def _withdraw(self): + if not self.winfo_exists(): + return if self.winfo_viewable() and self.hide: self.withdraw() @@ -229,13 +235,13 @@ def bind_autocomplete( def appear(x): self.appear = True - if self.attach.winfo_name().startswith("!ctkcombobox"): + if type(self.attach) is customtkinter.CTkComboBox: self.attach._entry.configure(textvariable=self.var_update) self.attach._entry.bind("", appear) self.attach.set(self.values[0]) self.var_update.trace_add("write", self._update) - if self.attach.winfo_name().startswith("!ctkentry"): + if type(self.attach) is customtkinter.CTkEntry: self.attach.configure(textvariable=self.var_update) self.attach.bind("", appear) self.var_update.trace_add("write", self._update) @@ -270,8 +276,9 @@ def _init_buttons(self, **button_kwargs): if self.image_values is not None else None, anchor=self.justify, + hover_color=self.hover_color, command=lambda k=row: self._attach_key_press(k), - **button_kwargs + **button_kwargs, ) self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) self.i += 1 @@ -317,12 +324,14 @@ def _iconify(self): return if self.disable: return + if self.winfo_ismapped(): + self.hide = False if self.hide: self.event_generate("<>") - self._deiconify() self.focus() self.hide = False self.place_dropdown() + self._deiconify() if self.focus_something: self.dummy_entry.pack() self.dummy_entry.focus_set() @@ -389,9 +398,10 @@ def insert(self, value, **kwargs): height=self.button_height, fg_color=self.button_color, text_color=self.text_color, + hover_color=self.hover_color, anchor=self.justify, command=lambda k=value: self._attach_key_press(k), - **kwargs + **kwargs, ) self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) self.i += 1 @@ -407,6 +417,9 @@ def popup(self, x=None, y=None): self.hide = True self._iconify() + def hide(self): + self._withdraw() + def configure(self, **kwargs): if "height" in kwargs: self.height = kwargs.pop("height") @@ -443,8 +456,14 @@ def configure(self, **kwargs): i += 1 if "button_color" in kwargs: + button_color = kwargs.pop("button_color") + for key in self.widgets.keys(): + self.widgets[key].configure(fg_color=button_color) + + if "font" in kwargs: + font = kwargs.pop("font") for key in self.widgets.keys(): - self.widgets[key].configure(fg_color=kwargs.pop("button_color")) + self.widgets[key].configure(font=font) if "hover_color" not in kwargs: kwargs["hover_color"] = self.hover_color diff --git a/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown_frame.py b/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown_frame.py index 0dcaff5..6e77cc4 100644 --- a/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown_frame.py +++ b/src/views/custom_widgets/ctk_scrollable_dropdown/ctk_scrollable_dropdown_frame.py @@ -34,7 +34,7 @@ def __init__( frame_border_color=None, text_color=None, autocomplete=False, - **button_kwargs + **button_kwargs, ): super().__init__( master=attach.winfo_toplevel(), bg_color=attach.cget("bg_color") @@ -56,6 +56,11 @@ def __init__( lambda e: self._withdraw() if not self.disable else None, add="+", ) + self.bind( + "", + lambda e: self._withdraw() if not self.disable else None, + add="+", + ) self.disable = False self.fg_color = ( @@ -192,7 +197,9 @@ def _withdraw(self): def _update(self, a, b, c): self.live_update(self.attach._entry.get()) - def bind_autocomplete(self): + def bind_autocomplete( + self, + ): def appear(x): self.appear = True @@ -222,7 +229,7 @@ def _init_buttons(self, **button_kwargs): else None, anchor=self.justify, command=lambda k=row: self._attach_key_press(k), - **button_kwargs + **button_kwargs, ) self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) self.i += 1 @@ -259,6 +266,7 @@ def place_dropdown(self): self.height_new = self.height self.frame.configure(width=self.width_new, height=self.height_new) + self.frame._scrollbar.configure(height=self.height_new) self.place(x=self.x_pos, y=self.y_pos) if sys.platform.startswith("darwin"): @@ -340,7 +348,7 @@ def insert(self, value, **kwargs): text_color=self.text_color, anchor=self.justify, command=lambda k=value: self._attach_key_press(k), - **kwargs + **kwargs, ) self.widgets[self.i].pack(fill="x", pady=2, padx=(self.padding, 0)) self.i += 1 @@ -392,8 +400,14 @@ def configure(self, **kwargs): i += 1 if "button_color" in kwargs: + button_color = kwargs.pop("button_color") + for key in self.widgets.keys(): + self.widgets[key].configure(fg_color=button_color) + + if "font" in kwargs: + font = kwargs.pop("font") for key in self.widgets.keys(): - self.widgets[key].configure(fg_color=kwargs.pop("button_color")) + self.widgets[key].configure(font=font) for key in self.widgets.keys(): self.widgets[key].configure(**kwargs) From e4359fdbf90d8eeee8355aca5849353b3a076402 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 17:48:25 +0200 Subject: [PATCH 13/81] Ignore mypy errors from the `ctk_scrollable_dropdown` module --- pyproject.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 07b42f4..93a2b96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,6 @@ [tool.mypy] disable_error_code = "import-untyped" + +[[tool.mypy.overrides]] +module = "*.ctk_scrollable_dropdown.*" +ignore_errors = true From 04e34a7a58777f6f500066c1f5d2444ad14916ae Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 21:34:32 +0200 Subject: [PATCH 14/81] Change the `section` and `key` parameter types of the `_on_config_change` method to `ConfigManager.KEY_TYPE` --- src/views/main_window.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index 8078284..accffc0 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -111,9 +111,9 @@ def _get_transcription_properties(self) -> dict[str, Any]: properties["should_translate"] = bool( self.chk_whisper_options_translate.get() ) - properties[ - "output_file_types" - ] = self._config_whisperx.output_file_types.split(",") + properties["output_file_types"] = ( + self._config_whisperx.output_file_types.split(",") + ) return properties @@ -1310,12 +1310,9 @@ def _toggle_progress_bar_visibility(self, should_show): self.progress_bar.grid_forget() def _toggle_frm_subtitle_options_visibility(self): - if ( - self._config_transcription.method == TranscriptionMethod.WHISPERX.value - and ( - "srt" in self._config_whisperx.output_file_types - or "vtt" in self._config_whisperx.output_file_types - ) + if self._config_transcription.method == TranscriptionMethod.WHISPERX.value and ( + "srt" in self._config_whisperx.output_file_types + or "vtt" in self._config_whisperx.output_file_types ): if "srt" in self._config_whisperx.output_file_types: self.chk_output_file_srt.select() @@ -1344,7 +1341,9 @@ def _change_appearance_mode_event(self, new_appearance_mode: str): ) @staticmethod - def _on_config_change(section: str, key: str, new_value: str): + def _on_config_change( + section: cm.ConfigManager.KeyType, key: cm.ConfigManager.KeyType, new_value: str + ): """ Updates a configuration value. It modifies the specified value in the configuration file using the `ConfigManager.modify_value` method. From 109abcbec468295f333fde478b8d90c4fe2ac0b5 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 21:41:27 +0200 Subject: [PATCH 15/81] Fix mypy errors in `WhisperXHandler` --- src/handlers/whisperx_handler.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/handlers/whisperx_handler.py b/src/handlers/whisperx_handler.py index fece35b..e1ad882 100644 --- a/src/handlers/whisperx_handler.py +++ b/src/handlers/whisperx_handler.py @@ -8,7 +8,7 @@ class WhisperXHandler: - def __init__(self): + def __init__(self) -> None: self._whisperx_result = None async def transcribe_file(self, transcription: Transcription) -> str: @@ -41,6 +41,9 @@ async def transcribe_file(self, transcription: Transcription) -> str: audio, batch_size=config_whisperx.batch_size ) + if self._whisperx_result is None: + raise ValueError("Something went wrong while transcribing.") + text_combined = " ".join( segment["text"].strip() for segment in self._whisperx_result["segments"] ) @@ -69,7 +72,7 @@ async def transcribe_file(self, transcription: Transcription) -> str: def save_transcription( self, file_path: Path, output_file_types: list[str], should_overwrite: bool - ): + ) -> None: """ Save the transcription as the specified file types. @@ -86,6 +89,9 @@ def save_transcription( the given format. :type should_overwrite: bool """ + if self._whisperx_result is None: + raise ValueError("Please generate the transcription again to save it.") + config_subtitles = cm.ConfigManager.get_config_subtitles() output_dir = file_path.parent From b633b0a3ac49da620285cf6629ed4a745db6447e Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 21:46:36 +0200 Subject: [PATCH 16/81] Add missing return statement to the `download_audio_from_video` method of `YouTubeHandler` --- src/handlers/youtube_handler.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/handlers/youtube_handler.py b/src/handlers/youtube_handler.py index 918107e..5f9688b 100644 --- a/src/handlers/youtube_handler.py +++ b/src/handlers/youtube_handler.py @@ -34,3 +34,4 @@ def download_audio_from_video( except Exception: print(traceback.format_exc()) + return None From f9b12d40fd13f182b1629d0ba7fc268252f4265b Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 21:47:40 +0200 Subject: [PATCH 17/81] Add missing optional type hint to `Transcription` --- src/models/transcription.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/transcription.py b/src/models/transcription.py index 8c11c49..e504653 100644 --- a/src/models/transcription.py +++ b/src/models/transcription.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import dataclass from pathlib import Path from typing import Optional @@ -16,4 +16,4 @@ class Transcription: should_translate: bool = False should_autosave: bool = False should_overwrite: bool = False - youtube_url: str = None + youtube_url: Optional[str] = None From 02403e4b0fa806369b3186bfdb9d799666ea5a2f Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 21:52:19 +0200 Subject: [PATCH 18/81] Add missing type annotation for the `transcription_func` parameter of the `get_transcription` method of `AudioHandler` --- src/handlers/audio_handler.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/handlers/audio_handler.py b/src/handlers/audio_handler.py index 5146492..46d3c43 100644 --- a/src/handlers/audio_handler.py +++ b/src/handlers/audio_handler.py @@ -2,6 +2,7 @@ import shutil import traceback from io import BytesIO +from typing import Callable import speech_recognition as sr from models.transcription import Transcription @@ -15,21 +16,27 @@ class AudioHandler: @staticmethod def get_transcription( - transcription: Transcription, should_split_on_silence: bool, transcription_func + transcription: Transcription, + should_split_on_silence: bool, + transcription_func: Callable[[sr.AudioData, Transcription], str], ) -> str: """ Transcribes audio from a file using the Google Speech-to-Text API. :param transcription: An instance of Transcription containing information about the audio file. + :type transcription: Transcription :param should_split_on_silence: A boolean flag indicating whether the audio should be split into chunks based on silence. If True, the audio will be split on silence and each chunk will be transcribed separately. If False, the entire audio will be transcribed as a single segment. + :type should_split_on_silence: bool :param transcription_func: The function to use for transcription. + :type transcription_func: Callable[[sr.AudioData, Transcription], str] :return: The transcribed text or an error message if transcription fails. + :rtype: str """ chunks_directory = ROOT_PATH / "audio-chunks" chunks_directory.mkdir(exist_ok=True) From 8f545821e5db2d2accbe4d3856afac45882676d6 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 21:55:27 +0200 Subject: [PATCH 19/81] Add missing return type annotation to the `load_audio_file` method of `AudioHandler` --- src/handlers/audio_handler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/audio_handler.py b/src/handlers/audio_handler.py index 46d3c43..0049a27 100644 --- a/src/handlers/audio_handler.py +++ b/src/handlers/audio_handler.py @@ -2,7 +2,7 @@ import shutil import traceback from io import BytesIO -from typing import Callable +from typing import Callable, Optional import speech_recognition as sr from models.transcription import Transcription @@ -66,7 +66,7 @@ def get_transcription( return text @staticmethod - def load_audio_file(file_path, chunks_directory): + def load_audio_file(file_path, chunks_directory) -> Optional[AudioSegment]: """ Load the audio from the file or extract it from the video. From 09b2fdaa777be8d94a1be4f8083e21b06bffc673 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 21:59:44 +0200 Subject: [PATCH 20/81] Add missing return type annotation to the `load_audio_file` method of `AudioHandler` --- src/handlers/audio_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/handlers/audio_handler.py b/src/handlers/audio_handler.py index 0049a27..23c7513 100644 --- a/src/handlers/audio_handler.py +++ b/src/handlers/audio_handler.py @@ -88,12 +88,14 @@ def load_audio_file(file_path, chunks_directory) -> Optional[AudioSegment]: return None @staticmethod - def split_audio_into_chunks(sound): + def split_audio_into_chunks(sound: AudioSegment) -> AudioSegment: """ Split the audio into chunks based on silence. :param sound: The AudioSegment object to be split. + :type sound: AudioSegment :return: List of audio chunks. + :rtype: AudioSegment """ return split_on_silence( sound, From cc2ae984ebbecfaca8f06ff4169529233db0820b Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 22:06:45 +0200 Subject: [PATCH 21/81] Add missing typing to the `process_audio_chunks` method of `AudioHandler` --- src/handlers/audio_handler.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/handlers/audio_handler.py b/src/handlers/audio_handler.py index 23c7513..007c0d2 100644 --- a/src/handlers/audio_handler.py +++ b/src/handlers/audio_handler.py @@ -2,6 +2,7 @@ import shutil import traceback from io import BytesIO +from pathlib import Path from typing import Callable, Optional import speech_recognition as sr @@ -107,18 +108,25 @@ def split_audio_into_chunks(sound: AudioSegment) -> AudioSegment: @staticmethod def process_audio_chunks( - audio_chunks, transcription, transcription_func, chunks_directory - ): + audio_chunks: list[AudioSegment], + transcription: Transcription, + transcription_func: Callable[[sr.AudioData, Transcription], str], + chunks_directory: Path, + ) -> str: """ Process each audio chunk for transcription. :param audio_chunks: List of audio chunks. + :type audio_chunks: list[AudioSegment] :param transcription: Transcription object containing transcription details. + :type transcription: Transcription :param transcription_func: The function to use for transcription. + :type transcription_func: Callable[[sr.AudioData, Transcription], str] :param chunks_directory: Directory to store intermediate audio files. + :type chunks_directory: Path :return: The combined transcribed text. + :rtype: str """ - text = "" recognizer = sr.Recognizer() for idx, audio_chunk in enumerate(audio_chunks): @@ -131,16 +139,16 @@ def process_audio_chunks( try: chunk_text = transcription_func( - audio_data=audio_data, - transcription=transcription, + audio_data, + transcription, ) - text += chunk_text print(f"chunk text: {chunk_text}") + return chunk_text except Exception: return traceback.format_exc() - return text + return "" @staticmethod def cleanup(chunks_directory): From 04fb5570ca4e1ec82221b282f7cef6720957f580 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 22:09:14 +0200 Subject: [PATCH 22/81] Add missing typing to the `cleanup` method of `AudioHandler` --- src/handlers/audio_handler.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/handlers/audio_handler.py b/src/handlers/audio_handler.py index 007c0d2..85a381d 100644 --- a/src/handlers/audio_handler.py +++ b/src/handlers/audio_handler.py @@ -151,11 +151,13 @@ def process_audio_chunks( return "" @staticmethod - def cleanup(chunks_directory): + def cleanup(chunks_directory: Path) -> None: """ Clean up the `chunks` directory. :param chunks_directory: Directory to be deleted. + :type chunks_directory: Path + :rtype: None """ shutil.rmtree(chunks_directory) From 88243b6a87bf569488949d3aa205c535c7ab8ed5 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Sun, 28 Jul 2024 22:10:27 +0200 Subject: [PATCH 23/81] Add missing typing to the `load_audio_file` method of `AudioHandler` --- src/handlers/audio_handler.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/handlers/audio_handler.py b/src/handlers/audio_handler.py index 85a381d..1cfc9b8 100644 --- a/src/handlers/audio_handler.py +++ b/src/handlers/audio_handler.py @@ -67,13 +67,18 @@ def get_transcription( return text @staticmethod - def load_audio_file(file_path, chunks_directory) -> Optional[AudioSegment]: + def load_audio_file( + file_path: Path, chunks_directory: Path + ) -> Optional[AudioSegment]: """ Load the audio from the file or extract it from the video. :param file_path: Path to the file to be loaded. + :type file_path: Path :param chunks_directory: Directory to store intermediate audio files. + :type chunks_directory: Path :return: Loaded AudioSegment object or None if unsupported file type. + :rtype: Optional[AudioSegment] """ content_type = file_path.suffix From 92eb02f53f0e24996207c1f786d3bd3e6b8851d8 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 11:29:23 +0200 Subject: [PATCH 24/81] Explicitly parse the return value of `recognize_google` to string in `GoogleApiHandler` --- src/handlers/google_api_handler.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/handlers/google_api_handler.py b/src/handlers/google_api_handler.py index 1fb4866..0e2e489 100644 --- a/src/handlers/google_api_handler.py +++ b/src/handlers/google_api_handler.py @@ -9,10 +9,12 @@ class GoogleApiHandler(Transcribable): def transcribe(audio_data: sr.AudioData, transcription: Transcription) -> str: r = sr.Recognizer() - text = r.recognize_google( - audio_data, - language=transcription.language_code, - key=EnvKeys.GOOGLE_API_KEY.get_value() or None, + text = str( + r.recognize_google( + audio_data, + language=transcription.language_code, + key=EnvKeys.GOOGLE_API_KEY.get_value() or None, + ) ) text = f"{text}. " From ae96837688e6568ed19b62d3416deca96d9ad0b6 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 11:35:38 +0200 Subject: [PATCH 25/81] Add type annotation to the `MainController` constructor --- src/controllers/main_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index 2f65ba1..11ae326 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -15,10 +15,11 @@ from models.transcription import Transcription from utils import constants as c from utils.enums import AudioSource, TranscriptionMethod +from views.main_window import MainWindow class MainController: - def __init__(self, transcription: Transcription, view): + def __init__(self, transcription: Transcription, view: MainWindow): self.view = view self.transcription = transcription self._is_mic_recording = False From 7ea22489334596407cf47bbff1fd72329f755e44 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 11:56:57 +0200 Subject: [PATCH 26/81] Add missing return type annotation to the methods of `MainController` --- src/controllers/main_controller.py | 56 +++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index 11ae326..8320ace 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -28,9 +28,11 @@ def __init__(self, transcription: Transcription, view: MainWindow): # PUBLIC METHODS - def select_file(self): + def select_file(self) -> None: """ Prompts a file explorer to determine the audio/video file path to transcribe. + + :return: None """ file_path = filedialog.askopenfilename( initialdir="/", @@ -45,16 +47,18 @@ def select_file(self): if file_path: self.view.on_select_path_success(file_path) - def select_directory(self): + def select_directory(self) -> None: """ Prompts a file explorer to determine the folder path to transcribe. + + :return: None """ dir_path = filedialog.askdirectory() if dir_path: self.view.on_select_path_success(dir_path) - def prepare_for_transcription(self, transcription: Transcription): + def prepare_for_transcription(self, transcription: Transcription) -> None: """ Prepares to transcribe based on the specified source type of the transcription object provided. It sets up the necessary configurations and starts the @@ -63,6 +67,7 @@ def prepare_for_transcription(self, transcription: Transcription): :param transcription: An instance of the Transcription class containing information about the audio to transcribe. :type transcription: Transcription + :return: None """ try: if not transcription.output_file_types: @@ -90,13 +95,22 @@ def prepare_for_transcription(self, transcription: Transcription): except Exception as e: self._handle_exception(e) - def stop_recording_from_mic(self): + def stop_recording_from_mic(self) -> None: + """ + Stops recording audio from the microphone. + + This method sets the `_is_mic_recording` attribute to False and triggers the + `on_stop_recording_from_mic` method on the `view` attribute to indicate that + recording from the microphone has stopped. + + :return: None + """ self._is_mic_recording = False self.view.on_stop_recording_from_mic() def save_transcription( self, file_path: Path, should_autosave: bool, should_overwrite: bool - ): + ) -> None: """ Saves the transcription to a text file and optionally generate subtitles. @@ -108,6 +122,7 @@ def save_transcription( :param should_overwrite: Indicates whether existing files should be overwritten if they exist. :type should_overwrite: bool + :return: None """ save_file_path = self._get_save_path(file_path, should_autosave) @@ -135,15 +150,17 @@ def save_transcription( # PRIVATE METHODS - def _prepare_for_file_transcription(self, file_path: Path): + def _prepare_for_file_transcription(self, file_path: Path) -> None: """ Prepares the system for transcription from a file by verifying if the file exists and is supported for transcription. If the file is valid, it updates the source path in the transcription object; otherwise, it raises a ValueError. :param file_path: The path to the file for transcription. + :type file_path: Path :raises ValueError: If the provided file path does not exist or is not supported for transcription. + :return: None """ is_file_supported = file_path.suffix in c.SUPPORTED_FILE_EXTENSIONS if file_path.is_file() and is_file_supported: @@ -151,14 +168,16 @@ def _prepare_for_file_transcription(self, file_path: Path): else: raise ValueError("Error: No valid file selected.") - def _prepare_for_youtube_video_transcription(self): + def _prepare_for_youtube_video_transcription(self) -> None: """ Prepares the system for transcription from a YouTube video by downloading the audio from the video using the YouTubeHandler. It updates the source path in the transcription object with the downloaded audio file path. If the source path is not obtained successfully, it raises a ValueError. - :raises ValueError: If the YouTube video URL is incorrect or the audio download fails. + :raises ValueError: If the YouTube video URL is incorrect or the audio download + fails. + :return: None """ self.transcription.audio_source_path = YouTubeHandler.download_audio_from_video( self.transcription.youtube_url @@ -167,12 +186,14 @@ def _prepare_for_youtube_video_transcription(self): if not self.transcription.audio_source_path: raise ValueError("Please make sure the URL you entered is correct.") - async def _handle_transcription_process(self): + async def _handle_transcription_process(self) -> None: """ Handles the transcription process based on the type of source specified in the transcription object. It asynchronously transcribes either a single file or multiple files in a directory. Upon completion or error, it notifies the view that the transcription process has been processed. + + :return: None """ try: if self.transcription.audio_source == AudioSource.DIRECTORY: @@ -184,15 +205,15 @@ async def _handle_transcription_process(self): finally: self.view.on_processed_transcription() - async def _transcribe_directory(self, dir_path: Path): + async def _transcribe_directory(self, dir_path: Path) -> None: """ Transcribes supported files from a directory. :param dir_path: The directory path selected by the user. :type dir_path: Path - :raises ValueError: If the directory path is invalid or doesn't contain valid file types to transcribe. + :return: None """ if files := self._get_files_to_transcribe_from_directory(): # Create a list of coroutines for each file transcription task @@ -208,7 +229,7 @@ async def _transcribe_directory(self, dir_path: Path): "file types to transcribe. Please choose another one." ) - async def _transcribe_file(self, file_path: Path): + async def _transcribe_file(self, file_path: Path) -> None: """ Transcribes audio from a file based on the specified transcription method. It updates the transcription object with the transcribed text. If the source @@ -217,6 +238,8 @@ async def _transcribe_file(self, file_path: Path): is enabled. :param file_path: The path of the audio file for transcription. + :type file_path: Path + :return: None """ transcription = self.transcription transcription.audio_source_path = file_path @@ -276,13 +299,15 @@ def _get_files_to_transcribe_from_directory(self) -> list[Path]: return matching_files - def _start_recording_from_mic(self): + def _start_recording_from_mic(self) -> None: """ Records the audio from the microphone and starts the transcription process when finished recording. This function continuously records audio from the microphone until stopped. The recorded audio is then saved to a WAV file and used for transcription. + + :return: None """ self._is_mic_recording = True audio_data = [] @@ -320,11 +345,9 @@ def _get_save_path(self, file_path: Path, should_autosave: bool) -> Path: :param file_path: The initial file path. :type file_path: Path - :param should_autosave: If True, saves the file automatically with a generated name. :type should_autosave: bool - :return: The path where the file should be saved. :rtype: Path """ @@ -361,13 +384,14 @@ def _get_save_path(self, file_path: Path, should_autosave: bool) -> Path: ) ) - def _handle_exception(self, e: Exception): + def _handle_exception(self, e: Exception) -> None: """ Prints the traceback of the exception, notifies the view that the transcription process has been processed, and displays a representation of the exception. :param e: The exception that occurred during the transcription process. :type e: Exception + :return: None """ print(traceback.format_exc()) self.view.on_processed_transcription() From 2f97c653d34ea00e091e8f23b3f6caf70cad8bf5 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 12:38:48 +0200 Subject: [PATCH 27/81] Add missing documentation and return type annotations to the methods of `MainWindow` --- src/views/main_window.py | 180 ++++++++++++++++++++++++++++----------- 1 file changed, 132 insertions(+), 48 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index accffc0..b6fe6db 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -71,12 +71,13 @@ def __init__( # GETTERS AND SETTERS - def set_controller(self, controller: MainController): + def set_controller(self, controller: MainController) -> None: """ Set the controller of the window. :param controller: View controller :type controller: MainController + :return: None """ self._controller = controller @@ -86,7 +87,7 @@ def _get_transcription_properties(self) -> dict[str, Any]: transcription properties. :return: A dictionary containing the transcription properties. - :rtype: dict + :rtype: dict[str, Any] """ language_code = du.find_key_by_value( dictionary=c.AUDIO_LANGUAGES, @@ -111,15 +112,20 @@ def _get_transcription_properties(self) -> dict[str, Any]: properties["should_translate"] = bool( self.chk_whisper_options_translate.get() ) - properties["output_file_types"] = ( - self._config_whisperx.output_file_types.split(",") - ) + properties[ + "output_file_types" + ] = self._config_whisperx.output_file_types.split(",") return properties # WIDGETS INITIALIZATION - def _init_sidebar(self): + def _init_sidebar(self) -> None: + """ + Initializes the sidebar widgets. + + :return: None + """ # Sidebar frame self.frm_sidebar = ctk.CTkScrollableFrame( master=self, width=230, corner_radius=0 @@ -719,7 +725,12 @@ def _init_sidebar(self): ) self.lbl_info.grid(row=8, column=0, padx=20, pady=(5, 10)) - def _init_main_content(self): + def _init_main_content(self) -> None: + """ + Initializes the widgets on the right side of the main window. + + :return: None + """ # Main entry frame self.frm_main_entry = ctk.CTkFrame(master=self, fg_color="transparent") self.frm_main_entry.grid(row=0, column=1, padx=20, pady=(20, 0), sticky=ctk.EW) @@ -809,19 +820,22 @@ def _init_main_content(self): # PUBLIC METHODS (called by the controller) - def on_select_path_success(self, filepath: str): + def on_select_path_success(self, path: str) -> None: """ Handles the successful selection of a file or directory path by updating the entry field with the selected file or directory path. - :param filepath: The selected file or directory path. - :type filepath: str + :param path: The selected file or directory path. + :type path: str + :return: None """ - self.ent_path.configure(textvariable=ctk.StringVar(self, filepath)) + self.ent_path.configure(textvariable=ctk.StringVar(self, path)) - def on_processed_transcription(self): + def on_processed_transcription(self) -> None: """ Re-enables disabled widgets after transcription processing is complete. + + :return: None """ self.ent_path.configure(state=ctk.NORMAL) self.omn_transcription_language.configure(state=ctk.NORMAL) @@ -831,13 +845,15 @@ def on_processed_transcription(self): self._toggle_progress_bar_visibility(should_show=False) - def on_stop_recording_from_mic(self): + def on_stop_recording_from_mic(self) -> None: """ Updates the state to indicate that recording from the microphone has stopped, notified by the controller. It also updates the button appearance to indicate that recording can be started again. Additionally, it delegates the task of stopping the recording to the controller. + + :return: None """ self._is_transcribing_from_mic = False @@ -848,13 +864,14 @@ def on_stop_recording_from_mic(self): state=ctk.DISABLED, ) - def display_text(self, text): + def display_text(self, text) -> None: """ Clears any existing text in the transcription text box to display the provided text. :param text: The text to be displayed in the transcription text box. :type text: str + :return: None """ self.tbx_transcription.delete("1.0", ctk.END) self.tbx_transcription.insert("0.0", text) @@ -868,7 +885,7 @@ def _setup_debounced_change( variable: ctk.Variable, callback: callable, *unused: tuple, - ): + ) -> None: """ Sets up a debounced callback for a variable change. @@ -887,6 +904,7 @@ def _setup_debounced_change( :param unused: Additional unused arguments that must be kept to prevent exceptions. :type unused: tuple + :return: None """ variable.trace_add( mode="write", @@ -902,7 +920,7 @@ def _on_change_debounced( variable: ctk.Variable, callback: callable, delay: int = 600, - ): + ) -> None: """ Handles debounced changes to a variable. @@ -919,8 +937,9 @@ def _on_change_debounced( :type variable: tkinter.Variable :param callback: The function to be executed after the debounce delay. :type callback: callable - :param delay: The debounce delay in milliseconds before executing the callback. + :param delay: Debounce delay in milliseconds before executing the callback. :type delay: int, optional + :return: None """ # Cancel the previously scheduled after call if self._after_id is not None: @@ -931,7 +950,17 @@ def _on_change_debounced( delay, lambda: callback(section, key, variable.get()) ) - def _on_transcription_language_change(self, option: str): + def _on_transcription_language_change(self, option: str) -> None: + """ + Handles changes to `omn_transcription_language`. + + Updates the configuration entry for the transcription language and sets the + value in the `omn_transcription_language`. + + :param option: The selected transcription language. + :type option: str + :return: None + """ self._on_config_change( section=ConfigTranscription.Key.SECTION, key=ConfigTranscription.Key.LANGUAGE, @@ -939,9 +968,9 @@ def _on_transcription_language_change(self, option: str): ) self.omn_transcription_language.set(option) - def _on_audio_source_change(self, option: str): + def _on_audio_source_change(self, option: str) -> None: """ - Handles changes to `omn_transcribe_from`. + Handles changes to `omn_audio_source`. Updates the transcription source based on the selected option. It also adjusts the GUI elements accordingly, such as configuring buttons, labels, and entry @@ -985,10 +1014,12 @@ def _on_audio_source_change(self, option: str): self.btn_file_explorer.grid_remove() self.frm_main_entry.grid() - def _on_select_path(self): + def _on_select_path(self) -> None: """ Triggers when `btn_file_explorer` is clicked to select the path of the file or directory to transcribe. + + :return: None """ if self._audio_source == AudioSource.FILE: self._controller.select_file() @@ -996,7 +1027,16 @@ def _on_select_path(self): self._controller.select_directory() @staticmethod - def _validate_temperature(temperature): + def _validate_temperature(temperature: str) -> bool: + """ + Validates the input value of temperature to ensure that it is within the correct + range (0.0 and 1.0). + + :param temperature: The input temperature to validate. + :type temperature: str + :return: True if the temperature is valid or False if it is not. + :rtype: bool + """ if temperature == "": return True @@ -1006,10 +1046,12 @@ def _validate_temperature(temperature): except ValueError: return False - def _on_start_recording_from_mic(self): + def _on_start_recording_from_mic(self) -> None: """ Updates the UI when the user has clicked the `btn_main_action` with the audio source set to Microphone. + + :return: None """ self._is_transcribing_from_mic = True @@ -1023,10 +1065,12 @@ def _on_start_recording_from_mic(self): state=ctk.NORMAL, ) - def _prepare_ui_for_transcription(self): + def _prepare_ui_for_transcription(self) -> None: """ Disables fields, shows the progress bar and removes the text of the previous transcription. + + :return: None """ self.ent_path.configure(state=ctk.DISABLED) self.omn_transcription_language.configure(state=ctk.DISABLED) @@ -1040,7 +1084,7 @@ def _prepare_ui_for_transcription(self): self.display_text("") - def _on_main_action(self): + def _on_main_action(self) -> None: """ Triggers when `btn_main_action` is clicked. @@ -1048,6 +1092,8 @@ def _on_main_action(self): selections in the user interface. It disables certain UI elements during the transcription process to prevent further user input until the transcription is complete. + + :return: None """ self._prepare_ui_for_transcription() @@ -1066,10 +1112,12 @@ def _on_main_action(self): self._controller.prepare_for_transcription(transcription) - def _on_save_transcription(self): + def _on_save_transcription(self) -> None: """ Triggers when `btn_save` is clicked. Prompts the user with the file explorer to select a directory and enter the name of the transcription file. + + :return: None """ self._controller.save_transcription( file_path=Path(self.ent_path.get()), @@ -1077,13 +1125,17 @@ def _on_save_transcription(self): should_overwrite=False, ) - def _on_transcription_method_change(self, option: str): + def _on_transcription_method_change(self, option: str) -> None: """ - Handles changes to the radio buttons of the "Transcribe using" option. + Handles changes to `omn_transcription_method`. Updates the user interface based on the chosen transcription method. It displays or hides specific options depending on whether WhisperX or Google API transcription method is selected. + + :param option: Selected transcription method. + :type option: str + :return: None """ self._on_config_change( section=ConfigTranscription.Key.SECTION, @@ -1118,13 +1170,16 @@ def _on_transcription_method_change(self, option: str): self.frm_whisper_api_options.grid() @staticmethod - def _on_set_api_key(env_key: EnvKeys, title: str): + def _on_set_api_key(env_key: EnvKeys, title: str) -> None: """ - Handles the setting of an API key depending on . - - Prompts the user to input a new Google API key through a dialog window. If a new - API key is provided, and it differs from the existing one, it updates the - configuration with the new API key. + Opens a dialog window to store the API key in the environment variables, if + applicable. + + :param env_key: Environment key to set the value. + :type env_key: EnvKeys + :param title: Title of the dialog window. + :type title: str + :return: None """ old_api_key = env_key.get_value() @@ -1139,7 +1194,7 @@ def _on_set_api_key(env_key: EnvKeys, title: str): if new_api_key and old_api_key != new_api_key: env_key.set_value(new_api_key.strip()) - def _on_show_advanced_options(self): + def _on_show_advanced_options(self) -> None: """ Handle clicks on `btn_whisperx_show_advanced_options`. @@ -1148,6 +1203,8 @@ def _on_show_advanced_options(self): the button text to "Show advanced options". If the advanced options frame is currently hidden, it displays the frame and updates the button text to "Hide advanced options". + + :return: None """ if self.frm_whisperx_advanced_options.winfo_ismapped(): self.frm_whisperx_advanced_options.grid_remove() @@ -1160,7 +1217,7 @@ def _on_show_advanced_options(self): text="Hide advanced options" ) - def _on_autosave_change(self): + def _on_autosave_change(self) -> None: """ Handles changes to `chk_autosave`. @@ -1168,6 +1225,8 @@ def _on_autosave_change(self): autosave option is selected or deselected. If `chk_autosave` is selected, it enables `chk_overwrite_files`. If `chk_autosave` is deselected, it deselects and disables `chk_overwrite_files`. + + :return: None """ self._on_config_change( section=ConfigTranscription.Key.SECTION, @@ -1188,10 +1247,13 @@ def _on_autosave_change(self): self.chk_overwrite_files.deselect() self.chk_overwrite_files.configure(state=ctk.DISABLED) - def _on_output_file_types_change(self): + def _on_output_file_types_change(self) -> None: """ Handles changes to the output file types by updating the configuration and - displaying the appropriate subtitle options. + displaying the appropriate subtitle options if any of the selected output file + types is a subtitle file type. + + :return: None """ # Dictionary mapping checkboxes to their corresponding file types checkbox_to_file_type = { @@ -1225,10 +1287,12 @@ def _on_output_file_types_change(self): new_value=output_file_types_str, ) - def _toggle_chk_timestamp_granularities(self): + def _toggle_chk_timestamp_granularities(self) -> None: """ Toggles timestamp granularities checkboxes visibility depending on the selected response format. + + :return: None """ if self.omn_response_format.get() != "verbose_json": self.chk_timestamp_granularities_segment.configure(state=ctk.DISABLED) @@ -1252,10 +1316,14 @@ def _toggle_chk_timestamp_granularities(self): ): self.chk_timestamp_granularities_word.select() - def _on_response_format_change(self, option: str): + def _on_response_format_change(self, option: str) -> None: """ Handles changes to the response format by updating the configuration and toggling the timestamp granularities checkboxes. + + :param option: Selected response format. + :type option: str + :return: None """ self._on_config_change( section=ConfigWhisperApi.Key.SECTION, @@ -1265,9 +1333,11 @@ def _on_response_format_change(self, option: str): self._toggle_chk_timestamp_granularities() - def _on_timestamp_granularities_change(self): + def _on_timestamp_granularities_change(self) -> None: """ Handles changes to the timestamp granularities by updating the configuration. + + :return: None """ # Dictionary mapping checkboxes to their corresponding file types chk_to_timestamp_granularity = { @@ -1297,11 +1367,13 @@ def _on_timestamp_granularities_change(self): new_value=selected_timestamp_granularities_str, ) - def _toggle_progress_bar_visibility(self, should_show): + def _toggle_progress_bar_visibility(self, should_show: bool) -> None: """ Toggles the visibility of the progress bar based on the specified parameter. :param should_show: A boolean indicating whether to show or hide the bar. + :type should_show: bool + :return: None """ if should_show: self.progress_bar.grid(row=2, column=1, padx=40, pady=0, sticky=ctk.EW) @@ -1309,10 +1381,20 @@ def _toggle_progress_bar_visibility(self, should_show): else: self.progress_bar.grid_forget() - def _toggle_frm_subtitle_options_visibility(self): - if self._config_transcription.method == TranscriptionMethod.WHISPERX.value and ( - "srt" in self._config_whisperx.output_file_types - or "vtt" in self._config_whisperx.output_file_types + def _toggle_frm_subtitle_options_visibility(self) -> None: + """ + Toggle the visibility of `frm_subtitle_options` depending on whether the + transcription method allows to configure subtitle generation and whether + any of the selected output file types is a subtitle file type. + + :return: None + """ + if ( + self._config_transcription.method == TranscriptionMethod.WHISPERX.value + and ( + "srt" in self._config_whisperx.output_file_types + or "vtt" in self._config_whisperx.output_file_types + ) ): if "srt" in self._config_whisperx.output_file_types: self.chk_output_file_srt.select() @@ -1323,7 +1405,7 @@ def _toggle_frm_subtitle_options_visibility(self): else: self.frm_subtitle_options.grid_remove() - def _change_appearance_mode_event(self, new_appearance_mode: str): + def _change_appearance_mode_event(self, new_appearance_mode: str) -> None: """ Changes the appearance mode of the application and stores it in the configuration file. @@ -1331,6 +1413,7 @@ def _change_appearance_mode_event(self, new_appearance_mode: str): :param new_appearance_mode: The new appearance mode to set for the application. It can be "Light", "Dark" or "System". :type new_appearance_mode: str + :return: None """ ctk.set_appearance_mode(new_appearance_mode) @@ -1343,7 +1426,7 @@ def _change_appearance_mode_event(self, new_appearance_mode: str): @staticmethod def _on_config_change( section: cm.ConfigManager.KeyType, key: cm.ConfigManager.KeyType, new_value: str - ): + ) -> None: """ Updates a configuration value. It modifies the specified value in the configuration file using the `ConfigManager.modify_value` method. @@ -1354,5 +1437,6 @@ def _on_config_change( :type key: str :param new_value: The new value to replace the existing one. :type new_value: str + :return: None """ cm.ConfigManager.modify_value(section, key, new_value) From 47c7b3626c38fa13d299677919239758f022ff8d Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 12:43:06 +0200 Subject: [PATCH 28/81] Fix `Module views.custom_widgets.ctk_scrollable_dropdown does not explicitly export attribute CTkScrollableDropdown` --- src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py b/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py index 370a977..4afed52 100644 --- a/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py +++ b/src/views/custom_widgets/ctk_scrollable_dropdown/__init__.py @@ -10,3 +10,5 @@ from .ctk_scrollable_dropdown import CTkScrollableDropdown from .ctk_scrollable_dropdown_frame import CTkScrollableDropdownFrame + +__all__ = ["CTkScrollableDropdown", "CTkScrollableDropdownFrame"] From e1c4b96b0c8521b0c374a42ba5729ba1edcc26d9 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 12:47:48 +0200 Subject: [PATCH 29/81] Silence `Class cannot subclass CTk (has type Any)` error because `customtkinter` is not typed --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index 4492df6..b4aa363 100644 --- a/src/app.py +++ b/src/app.py @@ -10,7 +10,7 @@ from views.main_window import MainWindow -class App(ctk.CTk): +class App(ctk.CTk): # type: ignore[misc] def __init__(self): super().__init__() From 1bace803ae2f724f380a115f08f28624e7da665f Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 12:48:55 +0200 Subject: [PATCH 30/81] Add missing return type annotation to the `__init__` of `App` --- src/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.py b/src/app.py index b4aa363..468b3ed 100644 --- a/src/app.py +++ b/src/app.py @@ -11,7 +11,7 @@ class App(ctk.CTk): # type: ignore[misc] - def __init__(self): + def __init__(self) -> None: super().__init__() # Get config_system to set the initial appearance mode From 6871681e56882ff468a8dbacea7af0efeca49c92 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 13:03:04 +0200 Subject: [PATCH 31/81] Ensure that `self.transcription.output_file_types` is not empty or None in the `save_transcription` method of `MainController` --- src/controllers/main_controller.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index 8320ace..37619f9 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -130,11 +130,18 @@ def save_transcription( return if self.transcription.method == TranscriptionMethod.WHISPERX: - self._whisperx_handler.save_transcription( - file_path=Path(save_file_path), - output_file_types=self.transcription.output_file_types, - should_overwrite=should_overwrite, - ) + if self.transcription.output_file_types: + self._whisperx_handler.save_transcription( + file_path=Path(save_file_path), + output_file_types=self.transcription.output_file_types, + should_overwrite=should_overwrite, + ) + else: + exception = ValueError( + "There are no output file types selected. Please select at least " + "one." + ) + self._handle_exception(exception) elif self.transcription.method in [ TranscriptionMethod.GOOGLE_API, TranscriptionMethod.WHISPER_API, From f405323ec1d43c30815715bab494bb1207092bb9 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 13:11:49 +0200 Subject: [PATCH 32/81] Ensure that `self.transcription.text` is not empty or None in the `save_transcription` method of `MainController` --- src/controllers/main_controller.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index 37619f9..e79a128 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -146,9 +146,15 @@ def save_transcription( TranscriptionMethod.GOOGLE_API, TranscriptionMethod.WHISPER_API, ]: - if should_overwrite or not os.path.exists(save_file_path): - with open(save_file_path, "w", encoding="utf-8") as file: - file.write(self.transcription.text) + if self.transcription.text: + if should_overwrite or not os.path.exists(save_file_path): + with open(save_file_path, "w", encoding="utf-8") as file: + file.write(self.transcription.text) + else: + exception = ValueError( + "There is no transcription available. Please generate it again." + ) + self._handle_exception(exception) else: exception = ValueError( "Incorrect transcription method. Please check the `config.ini` file." From bb8271fdd053db52455041347e1ba32df1070c9b Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 14:00:32 +0200 Subject: [PATCH 33/81] Ensure that `self.transcription.audio_source_path` is not empty or None in the `_prepare_for_youtube_video_transcription` method of `MainController` --- src/controllers/main_controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index e79a128..a7e1ca3 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -192,13 +192,15 @@ def _prepare_for_youtube_video_transcription(self) -> None: fails. :return: None """ - self.transcription.audio_source_path = YouTubeHandler.download_audio_from_video( + audio_source_path = YouTubeHandler.download_audio_from_video( self.transcription.youtube_url ) - if not self.transcription.audio_source_path: + if not audio_source_path: raise ValueError("Please make sure the URL you entered is correct.") + self.transcription.audio_source_path = audio_source_path + async def _handle_transcription_process(self) -> None: """ Handles the transcription process based on the type of source specified in the From c041ce43bd41a31e523045108bfccd98561fed88 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 14:10:57 +0200 Subject: [PATCH 34/81] Pass the YouTube video URL as a parameter to `_prepare_for_youtube_video_transcription` and make sure that there is a URL before calling the method in `MainController` --- src/controllers/main_controller.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index a7e1ca3..c468a6d 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -83,7 +83,10 @@ def prepare_for_transcription(self, transcription: Transcription) -> None: threading.Thread(target=self._start_recording_from_mic).start() return elif transcription.audio_source == AudioSource.YOUTUBE: - self._prepare_for_youtube_video_transcription() + if url := transcription.youtube_url: + self._prepare_for_youtube_video_transcription(url) + else: + raise ValueError("No YouTube video URL provided. Please enter one.") threading.Thread( target=lambda loop: loop.run_until_complete( @@ -181,23 +184,26 @@ def _prepare_for_file_transcription(self, file_path: Path) -> None: else: raise ValueError("Error: No valid file selected.") - def _prepare_for_youtube_video_transcription(self) -> None: + def _prepare_for_youtube_video_transcription(self, url: str) -> None: """ Prepares the system for transcription from a YouTube video by downloading the audio from the video using the YouTubeHandler. It updates the source path in the transcription object with the downloaded audio file path. If the source path is not obtained successfully, it raises a ValueError. + :param url: URL of the YouTube video to transcribe. + :type url: str :raises ValueError: If the YouTube video URL is incorrect or the audio download fails. :return: None """ - audio_source_path = YouTubeHandler.download_audio_from_video( - self.transcription.youtube_url - ) + audio_source_path = YouTubeHandler.download_audio_from_video(url) if not audio_source_path: - raise ValueError("Please make sure the URL you entered is correct.") + raise ValueError( + "Something went wrong with the YouTube video audio download. Please " + "make sure the URL you entered is correct." + ) self.transcription.audio_source_path = audio_source_path From 741503098bbf888128f47de705551c6816e4cbbf Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 15:26:15 +0200 Subject: [PATCH 35/81] Ensure that `self.transcription.output_file_types` is not empty or None in the `_get_files_to_transcribe_from_directory` method of `MainController` --- src/controllers/main_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index c468a6d..79d8417 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -302,12 +302,18 @@ def _get_files_to_transcribe_from_directory(self) -> list[Path]: :return: A list of file paths to transcribe in the directory. :rtype: list[Path] """ + if not self.transcription.output_file_types: + raise ValueError( + "No output file types selected. Please select at least one." + ) + matching_files = [] for root, _, files in os.walk(self.transcription.audio_source_path): for file in files: if any(file.endswith(ext) for ext in c.SUPPORTED_FILE_EXTENSIONS): file_path = Path(root) / file + if not self.transcription.should_overwrite and any( (file_path.with_suffix(f".{ext}")).exists() for ext in self.transcription.output_file_types From 1b8be92ed9a9007f68e44e9341c2314ad9595878 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 15:27:59 +0200 Subject: [PATCH 36/81] Modify logic to initialize `is_one_output_file_type` in the `_get_save_path` method of `MainController` --- src/controllers/main_controller.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index 79d8417..3930cf5 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -378,14 +378,18 @@ def _get_save_path(self, file_path: Path, should_autosave: bool) -> Path: :return: The path where the file should be saved. :rtype: Path """ + if self.transcription.output_file_types: + is_one_output_file_type = len(self.transcription.output_file_types) == 1 + else: + is_one_output_file_type = False + file_dir = file_path.parent file_type = "" initial_file_name = file_path.stem - is_one_output_file_type = len(self.transcription.output_file_types) == 1 if is_one_output_file_type: - file_type = c.FORMATS_TO_FILE_TYPES.get( - self.transcription.output_file_types[0] + file_type = c.FORMATS_TO_FILE_TYPES.get( # type: ignore[assignment] + self.transcription.output_file_types[0] # type: ignore[index] ) initial_file_name += f".{file_type}" From c39dee28aaaa878c5fc795f923b244442c9b43a1 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 16:07:33 +0200 Subject: [PATCH 37/81] Ignore `Class cannot subclass CTkFrame (has type Any)` error --- src/views/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index b6fe6db..bdf2b06 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -29,7 +29,7 @@ from .custom_widgets.ctk_scrollable_dropdown import CTkScrollableDropdown -class MainWindow(ctk.CTkFrame): +class MainWindow(ctk.CTkFrame): # type: ignore[misc] def __init__( self, parent, From ae1f96e2eb08ea79c9ff344808fa26e478c167c2 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 16:35:01 +0200 Subject: [PATCH 38/81] Add `Any` type to the `parent` parameter of the constructor of `MainWindow` --- src/views/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index bdf2b06..f46fb08 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -32,7 +32,7 @@ class MainWindow(ctk.CTkFrame): # type: ignore[misc] def __init__( self, - parent, + parent: Any, config_subtitles: ConfigSubtitles, config_system: ConfigSystem, config_transcription: ConfigTranscription, From 5dbc05c69bd3a8411c30dcf91408c9c1269a8215 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 16:36:42 +0200 Subject: [PATCH 39/81] Add type annotation for `self._controller` of `MainWindow` --- src/views/main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index f46fb08..bb04169 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Any +from typing import Any, Union import customtkinter as ctk import utils.config_manager as cm @@ -53,7 +53,7 @@ def __init__( self._config_whisperx = config_whisperx # Init the controller - self._controller = None + self._controller: Union[MainController, None] = None # Init the components of the window self._init_sidebar() From 2a34e291ff5615d97a9ab71ed6275d333a7efb64 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 17:20:29 +0200 Subject: [PATCH 40/81] Remove type annotation on `view` to avoid circular imports --- src/controllers/main_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/main_controller.py b/src/controllers/main_controller.py index 3930cf5..46d9232 100644 --- a/src/controllers/main_controller.py +++ b/src/controllers/main_controller.py @@ -15,11 +15,11 @@ from models.transcription import Transcription from utils import constants as c from utils.enums import AudioSource, TranscriptionMethod -from views.main_window import MainWindow class MainController: - def __init__(self, transcription: Transcription, view: MainWindow): + # Don't add type annotation to `view` to avoid circular imports + def __init__(self, transcription: Transcription, view): # type: ignore[no-untyped-def] self.view = view self.transcription = transcription self._is_mic_recording = False From c7d4ed4df19c7663673e76cc4fb29aed88862fd3 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 17:37:16 +0200 Subject: [PATCH 41/81] Move `_get_transcription_properties` after widgets initialization --- src/views/main_window.py | 74 ++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index bb04169..22ad059 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -81,43 +81,6 @@ def set_controller(self, controller: MainController) -> None: """ self._controller = controller - def _get_transcription_properties(self) -> dict[str, Any]: - """ - Checks the current state of user interface elements to determine the - transcription properties. - - :return: A dictionary containing the transcription properties. - :rtype: dict[str, Any] - """ - language_code = du.find_key_by_value( - dictionary=c.AUDIO_LANGUAGES, - target_value=self.omn_transcription_language.get(), - ) - - properties = { - "audio_source": self._audio_source, - "language_code": language_code, - "method": TranscriptionMethod(self.omn_transcription_method.get()), - "should_autosave": self.chk_autosave.get() == 1, - "should_overwrite": self.chk_overwrite_files.get() == 1, - } - - if self.omn_transcription_method.get() == TranscriptionMethod.GOOGLE_API.value: - properties["should_translate"] = False - properties["output_file_types"] = ["txt"] - if self.omn_transcription_method.get() == TranscriptionMethod.WHISPER_API.value: - properties["should_translate"] = False - properties["output_file_types"] = [self.omn_response_format.get()] - if self.omn_transcription_method.get() == TranscriptionMethod.WHISPERX.value: - properties["should_translate"] = bool( - self.chk_whisper_options_translate.get() - ) - properties[ - "output_file_types" - ] = self._config_whisperx.output_file_types.split(",") - - return properties - # WIDGETS INITIALIZATION def _init_sidebar(self) -> None: @@ -878,6 +841,43 @@ def display_text(self, text) -> None: # PRIVATE METHODS + def _get_transcription_properties(self) -> dict[str, Any]: + """ + Checks the current state of user interface elements to determine the + transcription properties. + + :return: A dictionary containing the transcription properties. + :rtype: dict[str, Any] + """ + language_code = du.find_key_by_value( + dictionary=c.AUDIO_LANGUAGES, + target_value=self.omn_transcription_language.get(), + ) + + properties = { + "audio_source": self._audio_source, + "language_code": language_code, + "method": TranscriptionMethod(self.omn_transcription_method.get()), + "should_autosave": self.chk_autosave.get() == 1, + "should_overwrite": self.chk_overwrite_files.get() == 1, + } + + if self.omn_transcription_method.get() == TranscriptionMethod.GOOGLE_API.value: + properties["should_translate"] = False + properties["output_file_types"] = ["txt"] + if self.omn_transcription_method.get() == TranscriptionMethod.WHISPER_API.value: + properties["should_translate"] = False + properties["output_file_types"] = [self.omn_response_format.get()] + if self.omn_transcription_method.get() == TranscriptionMethod.WHISPERX.value: + properties["should_translate"] = bool( + self.chk_whisper_options_translate.get() + ) + properties[ + "output_file_types" + ] = self._config_whisperx.output_file_types.split(",") + + return properties + def _setup_debounced_change( self, section: str, From 0775c3235eca4f33c1057b783c16e22aef0c7b54 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:14:29 +0200 Subject: [PATCH 42/81] Change the parameter type annotations of `section` and `key` of `_setup_debounced_change` to `KeyType` to fix incompatible types --- src/views/main_window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index 22ad059..2f2ebc0 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -880,8 +880,8 @@ def _get_transcription_properties(self) -> dict[str, Any]: def _setup_debounced_change( self, - section: str, - key: str, + section: cm.ConfigManager.KeyType, + key: cm.ConfigManager.KeyType, variable: ctk.Variable, callback: callable, *unused: tuple, @@ -915,8 +915,8 @@ def _setup_debounced_change( def _on_change_debounced( self, - section: str, - key: str, + section: cm.ConfigManager.KeyType, + key: cm.ConfigManager.KeyType, variable: ctk.Variable, callback: callable, delay: int = 600, From 36efb12d8ba18750c09446eb07428fd96dc53a40 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:20:16 +0200 Subject: [PATCH 43/81] Use `Callable` from `typing` instead of `callable` for type annotation in `MainWindow` --- src/views/main_window.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index 2f2ebc0..9cf77c3 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -1,8 +1,7 @@ from pathlib import Path -from typing import Any, Union +from typing import Any, Callable, Union import customtkinter as ctk -import utils.config_manager as cm import utils.constants as c import utils.dict_utils as du import utils.path_helper as ph @@ -14,6 +13,7 @@ from models.config.config_whisperx import ConfigWhisperX from models.transcription import Transcription from PIL import Image +from utils.config_manager import ConfigManager from utils.enums import ( AudioSource, Color, @@ -880,10 +880,10 @@ def _get_transcription_properties(self) -> dict[str, Any]: def _setup_debounced_change( self, - section: cm.ConfigManager.KeyType, - key: cm.ConfigManager.KeyType, + section: ConfigManager.KeyType, + key: ConfigManager.KeyType, variable: ctk.Variable, - callback: callable, + callback: Callable[[ConfigManager.KeyType, ConfigManager.KeyType, str], None], *unused: tuple, ) -> None: """ @@ -900,7 +900,7 @@ def _setup_debounced_change( :param variable: The tkinter variable to watch for changes. :type variable: tkinter.Variable :param callback: The callback function to be executed after the debounce delay. - :type callback: function + :type callback: Callable[[cm.ConfigManager.KeyType, cm.ConfigManager.KeyType, str], None] :param unused: Additional unused arguments that must be kept to prevent exceptions. :type unused: tuple @@ -915,10 +915,10 @@ def _setup_debounced_change( def _on_change_debounced( self, - section: cm.ConfigManager.KeyType, - key: cm.ConfigManager.KeyType, + section: ConfigManager.KeyType, + key: ConfigManager.KeyType, variable: ctk.Variable, - callback: callable, + callback: Callable[[ConfigManager.KeyType, ConfigManager.KeyType, str], None], delay: int = 600, ) -> None: """ @@ -936,7 +936,7 @@ def _on_change_debounced( :param variable: The tkinter variable being monitored for changes. :type variable: tkinter.Variable :param callback: The function to be executed after the debounce delay. - :type callback: callable + :type callback: Callable[[cm.ConfigManager.KeyType, cm.ConfigManager.KeyType, str], None] :param delay: Debounce delay in milliseconds before executing the callback. :type delay: int, optional :return: None @@ -1425,7 +1425,7 @@ def _change_appearance_mode_event(self, new_appearance_mode: str) -> None: @staticmethod def _on_config_change( - section: cm.ConfigManager.KeyType, key: cm.ConfigManager.KeyType, new_value: str + section: ConfigManager.KeyType, key: ConfigManager.KeyType, new_value: str ) -> None: """ Updates a configuration value. It modifies the specified value in the @@ -1439,4 +1439,4 @@ def _on_config_change( :type new_value: str :return: None """ - cm.ConfigManager.modify_value(section, key, new_value) + ConfigManager.modify_value(section, key, new_value) From 212692d05f1ff9ee71985cf290b50c12bce4a524 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:36:07 +0200 Subject: [PATCH 44/81] Add `_on_highlight_words_change` to fix the `Cannot determine type of chk_highlight_words` error --- src/views/main_window.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index 9cf77c3..8c2b6d2 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -221,11 +221,7 @@ def _init_sidebar(self) -> None: self.chk_highlight_words = ctk.CTkCheckBox( master=self.frm_subtitle_options, text="Highlight words", - command=lambda: self._on_config_change( - section=ConfigSubtitles.Key.SECTION, - key=ConfigSubtitles.Key.HIGHLIGHT_WORDS, - new_value="True" if self.chk_highlight_words.get() else "False", - ), + command=self._on_highlight_words_change, ) self.chk_highlight_words.grid(row=1, column=0, padx=20, pady=10, sticky=ctk.W) @@ -1046,6 +1042,15 @@ def _validate_temperature(temperature: str) -> bool: except ValueError: return False + def _on_highlight_words_change(self) -> None: + new_value = "True" if self.chk_highlight_words.get() else "False" + + self._on_config_change( + section=ConfigSubtitles.Key.SECTION, + key=ConfigSubtitles.Key.HIGHLIGHT_WORDS, + new_value=new_value, + ) + def _on_start_recording_from_mic(self) -> None: """ Updates the UI when the user has clicked the `btn_main_action` with the audio From 7b2df17b8f1579962544c066250797f85a88c4a2 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:46:52 +0200 Subject: [PATCH 45/81] Use the `args` tuple to get the value of the option menus inside the lambda --- src/views/main_window.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index 8c2b6d2..ec90c5b 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -587,7 +587,7 @@ def _init_sidebar(self) -> None: command=lambda *args: self._on_config_change( section=ConfigWhisperX.Key.SECTION, key=ConfigWhisperX.Key.MODEL_SIZE, - new_value=self.omn_model_size.get(), + new_value=args[0], ), ) self.omn_model_size.grid(row=2, column=0, padx=20, pady=(3, 10), sticky=ctk.EW) @@ -608,7 +608,7 @@ def _init_sidebar(self) -> None: command=lambda *args: self._on_config_change( section=ConfigWhisperX.Key.SECTION, key=ConfigWhisperX.Key.COMPUTE_TYPE, - new_value=self.omn_compute_type.get(), + new_value=args[0], ), ) self.omn_compute_type.grid( From 8a958a6b08d5bc60b709a3c9e8b7172d7c8f6cd9 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:48:01 +0200 Subject: [PATCH 46/81] Add `_on_use_cpu_change` to fix the `Cannot determine type of chk_use_cpu` error --- src/views/main_window.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index ec90c5b..30f7b8f 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -642,11 +642,7 @@ def _init_sidebar(self) -> None: self.chk_use_cpu = ctk.CTkCheckBox( master=self.frm_whisperx_advanced_options, text="Use CPU", - command=lambda: self._on_config_change( - section=ConfigWhisperX.Key.SECTION, - key=ConfigWhisperX.Key.USE_CPU, - new_value="True" if self.chk_use_cpu.get() else "False", - ), + command=self._on_use_cpu_change, ) self.chk_use_cpu.grid(row=6, column=0, padx=20, pady=(10, 16), sticky=ctk.W) @@ -1410,6 +1406,15 @@ def _toggle_frm_subtitle_options_visibility(self) -> None: else: self.frm_subtitle_options.grid_remove() + def _on_use_cpu_change(self) -> None: + new_value = "True" if self.chk_use_cpu.get() else "False" + + self._on_config_change( + section=ConfigWhisperX.Key.SECTION, + key=ConfigWhisperX.Key.USE_CPU, + new_value=new_value, + ) + def _change_appearance_mode_event(self, new_appearance_mode: str) -> None: """ Changes the appearance mode of the application and stores it in the From 4baee99a91c1ee172d388888c68d730a49dc49ea Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:50:57 +0200 Subject: [PATCH 47/81] Add `_on_overwrite_files_change` to fix the `Cannot determine type of chk_overwrite_files` error --- src/views/main_window.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index 30f7b8f..b235e5a 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -759,11 +759,7 @@ def _init_main_content(self) -> None: self.chk_overwrite_files = ctk.CTkCheckBox( master=self.frm_save_options, text="Overwrite existing files", - command=lambda: self._on_config_change( - section=ConfigTranscription.Key.SECTION, - key=ConfigTranscription.Key.OVERWRITE_FILES, - new_value=str(bool(self.chk_overwrite_files.get())), - ), + command=self._on_overwrite_files_change, ) self.chk_overwrite_files.grid(row=0, column=2, padx=0, pady=0) @@ -1248,6 +1244,15 @@ def _on_autosave_change(self) -> None: self.chk_overwrite_files.deselect() self.chk_overwrite_files.configure(state=ctk.DISABLED) + def _on_overwrite_files_change(self) -> None: + new_value = "True" if self.chk_overwrite_files.get() else "False" + + self._on_config_change( + section=ConfigTranscription.Key.SECTION, + key=ConfigTranscription.Key.OVERWRITE_FILES, + new_value=new_value, + ) + def _on_output_file_types_change(self) -> None: """ Handles changes to the output file types by updating the configuration and From 402269122f1406c5d24e9a545ebb3c07424c0055 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:52:00 +0200 Subject: [PATCH 48/81] Add type annotation to the `text` parameter of the `display_method` of `MainWindow` --- src/views/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index b235e5a..d5cd6ef 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -815,7 +815,7 @@ def on_stop_recording_from_mic(self) -> None: state=ctk.DISABLED, ) - def display_text(self, text) -> None: + def display_text(self, text: str) -> None: """ Clears any existing text in the transcription text box to display the provided text. From ee68f13aa5fa0ed2ec4c433c845293155679b817 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 19:53:38 +0200 Subject: [PATCH 49/81] Ignore `Missing type parameters for generic type tuple` for `*unused` --- src/views/main_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index d5cd6ef..49bddf3 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -872,7 +872,7 @@ def _setup_debounced_change( key: ConfigManager.KeyType, variable: ctk.Variable, callback: Callable[[ConfigManager.KeyType, ConfigManager.KeyType, str], None], - *unused: tuple, + *unused: tuple, # type: ignore[type-arg] ) -> None: """ Sets up a debounced callback for a variable change. From 9e32559df67f459f330090be46a139884b62660b Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Mon, 29 Jul 2024 23:58:55 +0200 Subject: [PATCH 50/81] Assert that `self._controller` has a value when accessing its methods in `MainWindow` --- src/views/main_window.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/views/main_window.py b/src/views/main_window.py index 49bddf3..88c5901 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -1009,6 +1009,8 @@ def _on_select_path(self) -> None: :return: None """ + assert self._controller + if self._audio_source == AudioSource.FILE: self._controller.select_file() elif self._audio_source == AudioSource.DIRECTORY: @@ -1092,6 +1094,8 @@ def _on_main_action(self) -> None: :return: None """ + assert self._controller + self._prepare_ui_for_transcription() transcription = Transcription(**self._get_transcription_properties()) @@ -1116,6 +1120,8 @@ def _on_save_transcription(self) -> None: :return: None """ + assert self._controller + self._controller.save_transcription( file_path=Path(self.ent_path.get()), should_autosave=False, From 4dee0b4dc68981359b1e81a04c8fe35359bdaa76 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 00:06:59 +0200 Subject: [PATCH 51/81] Ensure that `transcription.language_code` has a value in the `transcribe` method of `OpenAiApiHandler` --- src/handlers/openai_api_handler.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/handlers/openai_api_handler.py b/src/handlers/openai_api_handler.py index 4b53c36..4a55817 100644 --- a/src/handlers/openai_api_handler.py +++ b/src/handlers/openai_api_handler.py @@ -11,6 +11,11 @@ class OpenAiApiHandler(Transcribable): @staticmethod def transcribe(audio_data: sr.AudioData, transcription: Transcription) -> str: + if not transcription.language_code: + raise ValueError( + "The language provided is not correct. Please select one of the list." + ) + config = cm.ConfigManager.get_config_whisper_api() compressed_audio = AudioHandler.compress_audio(audio_data) timestamp_granularities = ( From d43c4aa182051436d291114c7afc3415310ccab8 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 00:29:28 +0200 Subject: [PATCH 52/81] Type annotate `response_format` as `Literal[json, text, srt, verbose_json, vtt]` --- src/models/config/config_whisper_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/models/config/config_whisper_api.py b/src/models/config/config_whisper_api.py index 36d3e7f..328910c 100644 --- a/src/models/config/config_whisper_api.py +++ b/src/models/config/config_whisper_api.py @@ -1,11 +1,11 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional +from typing import Literal, Optional @dataclass class ConfigWhisperApi: - response_format: str + response_format: Literal["json", "text", "srt", "verbose_json", "vtt"] temperature: float timestamp_granularities: str From 6ba788b1f6afe4f40a75d845f5b88aad49979ccf Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 12:04:43 +0200 Subject: [PATCH 53/81] Remove optional return type annotation from the method `value_type` of the config models --- src/models/config/config_subtitles.py | 5 ++--- src/models/config/config_system.py | 5 ++--- src/models/config/config_transcription.py | 5 ++--- src/models/config/config_whisper_api.py | 6 +++--- src/models/config/config_whisperx.py | 6 +++--- 5 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/models/config/config_subtitles.py b/src/models/config/config_subtitles.py index 008a1a5..9196773 100644 --- a/src/models/config/config_subtitles.py +++ b/src/models/config/config_subtitles.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional @dataclass @@ -19,7 +18,7 @@ class Key(Enum): MAX_LINE_COUNT = "max_line_count" MAX_LINE_WIDTH = "max_line_width" - def value_type(self) -> Optional[str]: + def value_type(self) -> str: """ Get the value type associated with the ConfigKey. @@ -32,4 +31,4 @@ def value_type(self) -> Optional[str]: ConfigSubtitles.Key.MAX_LINE_WIDTH: "int", } - return str(type_mapping.get(self, None)) + return str(type_mapping.get(self)) diff --git a/src/models/config/config_system.py b/src/models/config/config_system.py index 417d027..c18ea4b 100644 --- a/src/models/config/config_system.py +++ b/src/models/config/config_system.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional @dataclass @@ -15,7 +14,7 @@ class Key(Enum): SECTION = "system" APPEARANCE_MODE = "appearance_mode" - def value_type(self) -> Optional[str]: + def value_type(self) -> str: """ Get the value type associated with the ConfigKey. @@ -24,4 +23,4 @@ def value_type(self) -> Optional[str]: """ type_mapping = {ConfigSystem.Key.APPEARANCE_MODE: "str"} - return str(type_mapping.get(self, None)) + return str(type_mapping.get(self)) diff --git a/src/models/config/config_transcription.py b/src/models/config/config_transcription.py index 82dee7f..9c6d82e 100644 --- a/src/models/config/config_transcription.py +++ b/src/models/config/config_transcription.py @@ -1,6 +1,5 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional @dataclass @@ -23,7 +22,7 @@ class Key(Enum): AUTOSAVE = "autosave" OVERWRITE_FILES = "overwrite_files" - def value_type(self) -> Optional[str]: + def value_type(self) -> str: """ Get the value type associated with the ConfigKey. @@ -38,4 +37,4 @@ def value_type(self) -> Optional[str]: ConfigTranscription.Key.OVERWRITE_FILES: "bool", } - return str(type_mapping.get(self, None)) + return str(type_mapping.get(self)) diff --git a/src/models/config/config_whisper_api.py b/src/models/config/config_whisper_api.py index 328910c..3d1257c 100644 --- a/src/models/config/config_whisper_api.py +++ b/src/models/config/config_whisper_api.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import Enum -from typing import Literal, Optional +from typing import Literal @dataclass @@ -19,7 +19,7 @@ class Key(Enum): TEMPERATURE = "temperature" TIMESTAMP_GRANULARITIES = "timestamp_granularities" - def value_type(self) -> Optional[str]: + def value_type(self) -> str: """ Get the value type associated with the ConfigKey. @@ -32,4 +32,4 @@ def value_type(self) -> Optional[str]: ConfigWhisperApi.Key.TIMESTAMP_GRANULARITIES: "str", } - return str(type_mapping.get(self, None)) + return str(type_mapping.get(self)) diff --git a/src/models/config/config_whisperx.py b/src/models/config/config_whisperx.py index 0deb2d4..d9d82cc 100644 --- a/src/models/config/config_whisperx.py +++ b/src/models/config/config_whisperx.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional +from typing import Literal @dataclass @@ -25,7 +25,7 @@ class Key(Enum): CAN_USE_GPU = "can_use_gpu" OUTPUT_FILE_TYPES = "output_file_types" - def value_type(self) -> Optional[str]: + def value_type(self) -> str: """ Get the value type associated with the ConfigKey. @@ -41,4 +41,4 @@ def value_type(self) -> Optional[str]: ConfigWhisperX.Key.OUTPUT_FILE_TYPES: "str", } - return str(type_mapping.get(self, None)) + return str(type_mapping.get(self)) From e54587cfe48d34e4de631e804b98cbf9a3b14658 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:03:10 +0200 Subject: [PATCH 54/81] Change `TEXT` to `TXT` in `WhisperXFileFormats` --- src/utils/enums.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/enums.py b/src/utils/enums.py index 9fcf650..ce0b7da 100644 --- a/src/utils/enums.py +++ b/src/utils/enums.py @@ -59,6 +59,6 @@ class WhisperXFileTypes(Enum): AUD = "aud" JSON = "json" SRT = "srt" - TEXT = "txt" + TXT = "txt" TSV = "tsv" VTT = "vtt" From f8cbb89872a3ee27ae1ce3c971392003481fd4f1 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:07:10 +0200 Subject: [PATCH 55/81] Ignore `Unsupported target for indexed assignment` for `self._whisperx_result[language] = en` --- src/handlers/whisperx_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/whisperx_handler.py b/src/handlers/whisperx_handler.py index e1ad882..300b393 100644 --- a/src/handlers/whisperx_handler.py +++ b/src/handlers/whisperx_handler.py @@ -102,6 +102,6 @@ def save_transcription( writer = whisperx.transcribe.get_writer(output_type, str(output_dir)) # https://github.com/m-bain/whisperX/issues/455#issuecomment-1707547704 - self._whisperx_result["language"] = "en" + self._whisperx_result["language"] = "en" # type: ignore[index] writer(self._whisperx_result, file_path, vars(config_subtitles)) From 89523662ad057e201aafc08d13669b09a0b8314e Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:08:01 +0200 Subject: [PATCH 56/81] Add type to `_whisperx_result` --- src/handlers/whisperx_handler.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/handlers/whisperx_handler.py b/src/handlers/whisperx_handler.py index 300b393..8deb5a7 100644 --- a/src/handlers/whisperx_handler.py +++ b/src/handlers/whisperx_handler.py @@ -1,15 +1,19 @@ import os import traceback from pathlib import Path +from typing import Optional, Union import utils.config_manager as cm import whisperx from models.transcription import Transcription +from whisperx.types import AlignedTranscriptionResult, TranscriptionResult class WhisperXHandler: def __init__(self) -> None: - self._whisperx_result = None + self._whisperx_result: Optional[ + Union[TranscriptionResult, AlignedTranscriptionResult] + ] = None async def transcribe_file(self, transcription: Transcription) -> str: """ From a11681309281171b17ff2ab0a7b318202b35fb0a Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:09:11 +0200 Subject: [PATCH 57/81] Remove unnecessary None check in the `save_transcription` method of `WhisperXHandler --- src/handlers/whisperx_handler.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/handlers/whisperx_handler.py b/src/handlers/whisperx_handler.py index 8deb5a7..159bcd0 100644 --- a/src/handlers/whisperx_handler.py +++ b/src/handlers/whisperx_handler.py @@ -93,9 +93,6 @@ def save_transcription( the given format. :type should_overwrite: bool """ - if self._whisperx_result is None: - raise ValueError("Please generate the transcription again to save it.") - config_subtitles = cm.ConfigManager.get_config_subtitles() output_dir = file_path.parent From bba15fd5a6ab554b92224a00cc74cc5f8a4806b3 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:09:54 +0200 Subject: [PATCH 58/81] Check that `transcription.output_file_types` has a value in the `transcribe_file` method of `WhisperXHandler` --- src/handlers/whisperx_handler.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/handlers/whisperx_handler.py b/src/handlers/whisperx_handler.py index 159bcd0..98ec852 100644 --- a/src/handlers/whisperx_handler.py +++ b/src/handlers/whisperx_handler.py @@ -25,6 +25,12 @@ async def transcribe_file(self, transcription: Transcription) -> str: :return: The transcribed text or an error message if transcription fails. :rtype: str """ + if not transcription.output_file_types: + raise ValueError( + "No output file types specified. Please make sure to select at least " + "one." + ) + config_whisperx = cm.ConfigManager.get_config_whisperx() device = "cpu" if config_whisperx.use_cpu else "cuda" From dd55ac7a5aad5f009a06f8b37ee7cebbded92dbb Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:10:36 +0200 Subject: [PATCH 59/81] Format parameters of the `transcribe_file` method of `WhisperXHandler` --- src/handlers/whisperx_handler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/handlers/whisperx_handler.py b/src/handlers/whisperx_handler.py index 98ec852..d608b59 100644 --- a/src/handlers/whisperx_handler.py +++ b/src/handlers/whisperx_handler.py @@ -81,7 +81,10 @@ async def transcribe_file(self, transcription: Transcription) -> str: return traceback.format_exc() def save_transcription( - self, file_path: Path, output_file_types: list[str], should_overwrite: bool + self, + file_path: Path, + output_file_types: list[str], + should_overwrite: bool, ) -> None: """ Save the transcription as the specified file types. From 84d7ff7b10a4784911c78a580bc31c9951e99889 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:13:04 +0200 Subject: [PATCH 60/81] Add `list` converter to `ConfigParser` --- src/utils/config_manager.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/utils/config_manager.py b/src/utils/config_manager.py index b7762e8..1c3cb42 100644 --- a/src/utils/config_manager.py +++ b/src/utils/config_manager.py @@ -22,7 +22,13 @@ class ConfigManager: @staticmethod def read_config(file_path: Path = _CONFIG_FILE_PATH) -> Optional[ConfigParser]: - config = ConfigParser() + config = ConfigParser( + converters={ + "list": lambda x: [i.strip() for i in x.split(",")] + if len(x) > 0 + else [] + } + ) config.read(file_path) return config @@ -125,11 +131,9 @@ def get_value( return config.getint(section_name, key_name) elif key_value_type == "float": return config.getfloat(section_name, key_name) - else: - print( - f"Section [{section_name}] or Key [{key_name}] not found in the config" - ) - return None + elif key_value_type == "list": + return config.getlist(section_name, key_name) # type: ignore + @staticmethod def modify_value( From 604cecee66c227e84994a4dd68cc2142cc0e0ff2 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:16:12 +0200 Subject: [PATCH 61/81] Change the return type annotation of `read_config` to `ConfigParser` instead of `Optional[ConfigParser]` --- src/utils/config_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/config_manager.py b/src/utils/config_manager.py index 1c3cb42..33f5b19 100644 --- a/src/utils/config_manager.py +++ b/src/utils/config_manager.py @@ -1,6 +1,6 @@ from configparser import ConfigParser from pathlib import Path -from typing import Optional, Union +from typing import Any, Union from models.config.config_subtitles import ConfigSubtitles from models.config.config_system import ConfigSystem @@ -21,7 +21,7 @@ class ConfigManager: ] @staticmethod - def read_config(file_path: Path = _CONFIG_FILE_PATH) -> Optional[ConfigParser]: + def read_config(file_path: Path = _CONFIG_FILE_PATH) -> ConfigParser: config = ConfigParser( converters={ "list": lambda x: [i.strip() for i in x.split(",")] From 5905f63ac9d4b2f8b3a408a2aaec6a79f8bfcba6 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:24:32 +0200 Subject: [PATCH 62/81] Add missing documentation to `get_value` and `modify_value` --- src/utils/config_manager.py | 47 +++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/src/utils/config_manager.py b/src/utils/config_manager.py index 33f5b19..b4ec0e5 100644 --- a/src/utils/config_manager.py +++ b/src/utils/config_manager.py @@ -114,7 +114,27 @@ def get_value( section: KeyType, key: KeyType, file_path: Path = _CONFIG_FILE_PATH, - ) -> Optional[Union[str, bool, int, float]]: + ) -> Union[str, bool, int, float, list[Any]]: + """ + Retrieve the value of a specified key within a section of a configuration file. + + This method reads a configuration file, checks if the given section and key exist, + and if they do, returns the value of the key in its appropriate type. If the + section or key does not exist, it raises a ValueError. + + :param section: The section in the configuration file where the key is located. + :type section: KeyType + :param key: The key within the section whose value is to be retrieved. + :type key: KeyType + :param file_path: The path to the configuration file. Defaults to + _CONFIG_FILE_PATH. + :type file_path: Path + :raises FileNotFoundError: If the specified configuration file does not exist. + :raises ValueError: If the section or key is not found in the config. file. + :return: The value of the specified key in its appropriate type (str, bool, int, + float, or list). + :rtype: Union[str, bool, int, float, list[Any]] + """ config = ConfigManager.read_config(file_path) section_name = section.value @@ -134,6 +154,9 @@ def get_value( elif key_value_type == "list": return config.getlist(section_name, key_name) # type: ignore + raise ValueError( + f"Section [{section}] or Key [{key_name}] not found in the config" + ) @staticmethod def modify_value( @@ -141,7 +164,27 @@ def modify_value( key: KeyType, new_value: str, file_path: Path = _CONFIG_FILE_PATH, - ): + ) -> None: + """ + Modify the value of a specified key within a section of a configuration file. + + This method reads a configuration file, checks if the given section and key + exist, and if they do, updates the value of the key to the new value provided. + If the section or key does not exist, it prints an error message. + + :param section: The section in the configuration file where the key is located. + :type section: KeyType + :param key: The key within the section whose value is to be modified. + :type key: KeyType + :param new_value: The new value to be set for the specified key. + :type new_value: str + :param file_path: The path to the configuration file. Defaults to + _CONFIG_FILE_PATH. + :type file_path: Path + :raises FileNotFoundError: If the specified configuration file does not exist. + :raises ValueError: If the section or key is not found in the config. file. + :return: None + """ config = ConfigManager.read_config(file_path) section_name = section.value From 55357bf15e199fa09050b9c9a4bef586330333a5 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:26:35 +0200 Subject: [PATCH 63/81] Parse `section_name` and `key_name` to prevent mypy type error --- src/utils/config_manager.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/utils/config_manager.py b/src/utils/config_manager.py index b4ec0e5..321c34b 100644 --- a/src/utils/config_manager.py +++ b/src/utils/config_manager.py @@ -137,8 +137,8 @@ def get_value( """ config = ConfigManager.read_config(file_path) - section_name = section.value - key_name = key.value + section_name = str(section.value) + key_name = str(key.value) key_value_type = key.value_type() # Check if the section and key exist before getting the value @@ -187,8 +187,8 @@ def modify_value( """ config = ConfigManager.read_config(file_path) - section_name = section.value - key_name = key.value + section_name = str(section.value) + key_name = str(key.value) # Check if the section and option exist before modifying if section_name in config and key_name in config[section_name]: @@ -197,6 +197,8 @@ def modify_value( with open(file_path, "w") as config_file: config.write(config_file) - print(f"Value for [{section}][{key_name}] modified to {new_value}") + print(f"Value for [{section_name}][{key_name}] modified to {new_value}") else: - print(f"Section [{section}] or Key [{key_name}] not found in the config") + print( + f"Section [{section_name}] or Key [{key_name}] not found in the config" + ) From 7fbf1e5247a702bc71a83402b4d955fa32908ffc Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:27:39 +0200 Subject: [PATCH 64/81] Ignore the type of the results of the `get_value` method of `ConfigManager`, as mypy is not able to infer their type --- src/utils/config_manager.py | 48 +++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/src/utils/config_manager.py b/src/utils/config_manager.py index 321c34b..fef5494 100644 --- a/src/utils/config_manager.py +++ b/src/utils/config_manager.py @@ -37,13 +37,13 @@ def get_config_subtitles() -> ConfigSubtitles: section = ConfigSubtitles.Key.SECTION return ConfigSubtitles( - highlight_words=ConfigManager.get_value( + highlight_words=ConfigManager.get_value( # type: ignore section, ConfigSubtitles.Key.HIGHLIGHT_WORDS ), - max_line_count=ConfigManager.get_value( + max_line_count=ConfigManager.get_value( # type: ignore section, ConfigSubtitles.Key.MAX_LINE_COUNT ), - max_line_width=ConfigManager.get_value( + max_line_width=ConfigManager.get_value( # type: ignore section, ConfigSubtitles.Key.MAX_LINE_WIDTH ), ) @@ -53,7 +53,7 @@ def get_config_system() -> ConfigSystem: section = ConfigSystem.Key.SECTION return ConfigSystem( - appearance_mode=ConfigManager.get_value( + appearance_mode=ConfigManager.get_value( # type: ignore section, ConfigSystem.Key.APPEARANCE_MODE ), ) @@ -63,13 +63,19 @@ def get_config_transcription() -> ConfigTranscription: section = ConfigTranscription.Key.SECTION return ConfigTranscription( - language=ConfigManager.get_value(section, ConfigTranscription.Key.LANGUAGE), - audio_source=ConfigManager.get_value( + language=ConfigManager.get_value( # type: ignore + section, ConfigTranscription.Key.LANGUAGE + ), + audio_source=ConfigManager.get_value( # type: ignore section, ConfigTranscription.Key.AUDIO_SOURCE ), - method=ConfigManager.get_value(section, ConfigTranscription.Key.METHOD), - autosave=ConfigManager.get_value(section, ConfigTranscription.Key.AUTOSAVE), - overwrite_files=ConfigManager.get_value( + method=ConfigManager.get_value( # type: ignore + section, ConfigTranscription.Key.METHOD + ), + autosave=ConfigManager.get_value( # type: ignore + section, ConfigTranscription.Key.AUTOSAVE + ), + overwrite_files=ConfigManager.get_value( # type: ignore section, ConfigTranscription.Key.OVERWRITE_FILES ), ) @@ -79,13 +85,13 @@ def get_config_whisper_api() -> ConfigWhisperApi: section = ConfigWhisperApi.Key.SECTION return ConfigWhisperApi( - response_format=ConfigManager.get_value( + response_format=ConfigManager.get_value( # type: ignore section, ConfigWhisperApi.Key.RESPONSE_FORMAT ), - temperature=ConfigManager.get_value( + temperature=ConfigManager.get_value( # type: ignore section, ConfigWhisperApi.Key.TEMPERATURE ), - timestamp_granularities=ConfigManager.get_value( + timestamp_granularities=ConfigManager.get_value( # type: ignore section, ConfigWhisperApi.Key.TIMESTAMP_GRANULARITIES ), ) @@ -95,16 +101,22 @@ def get_config_whisperx() -> ConfigWhisperX: section = ConfigWhisperX.Key.SECTION return ConfigWhisperX( - model_size=ConfigManager.get_value(section, ConfigWhisperX.Key.MODEL_SIZE), - batch_size=ConfigManager.get_value(section, ConfigWhisperX.Key.BATCH_SIZE), - compute_type=ConfigManager.get_value( + model_size=ConfigManager.get_value( # type: ignore + section, ConfigWhisperX.Key.MODEL_SIZE + ), + batch_size=ConfigManager.get_value( # type: ignore + section, ConfigWhisperX.Key.BATCH_SIZE + ), + compute_type=ConfigManager.get_value( # type: ignore section, ConfigWhisperX.Key.COMPUTE_TYPE ), - use_cpu=ConfigManager.get_value(section, ConfigWhisperX.Key.USE_CPU), - can_use_gpu=ConfigManager.get_value( + use_cpu=ConfigManager.get_value( # type: ignore + section, ConfigWhisperX.Key.USE_CPU + ), + can_use_gpu=ConfigManager.get_value( # type: ignore section, ConfigWhisperX.Key.CAN_USE_GPU ), - output_file_types=ConfigManager.get_value( + output_file_types=ConfigManager.get_value( # type: ignore section, ConfigWhisperX.Key.OUTPUT_FILE_TYPES ), ) From a1240c2e13966cbcc9d21e37f44ee8049aa8c211 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:28:55 +0200 Subject: [PATCH 65/81] Add `TimestampGranularitiesType` --- src/models/config/config_whisper_api.py | 6 ++++-- src/views/main_window.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/models/config/config_whisper_api.py b/src/models/config/config_whisper_api.py index 3d1257c..1432138 100644 --- a/src/models/config/config_whisper_api.py +++ b/src/models/config/config_whisper_api.py @@ -2,12 +2,14 @@ from enum import Enum from typing import Literal +TimestampGranularitiesType = Literal["word", "segment"] + @dataclass class ConfigWhisperApi: response_format: Literal["json", "text", "srt", "verbose_json", "vtt"] temperature: float - timestamp_granularities: str + timestamp_granularities: list[TimestampGranularitiesType] class Key(Enum): """ @@ -29,7 +31,7 @@ def value_type(self) -> str: type_mapping = { ConfigWhisperApi.Key.RESPONSE_FORMAT: "str", ConfigWhisperApi.Key.TEMPERATURE: "float", - ConfigWhisperApi.Key.TIMESTAMP_GRANULARITIES: "str", + ConfigWhisperApi.Key.TIMESTAMP_GRANULARITIES: "list", } return str(type_mapping.get(self)) diff --git a/src/views/main_window.py b/src/views/main_window.py index 88c5901..15fd180 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -9,8 +9,10 @@ from models.config.config_subtitles import ConfigSubtitles from models.config.config_system import ConfigSystem from models.config.config_transcription import ConfigTranscription -from models.config.config_whisper_api import ConfigWhisperApi -from models.config.config_whisperx import ConfigWhisperX +from models.config.config_whisper_api import ( + ConfigWhisperApi, + TimestampGranularitiesType, +) from models.transcription import Transcription from PIL import Image from utils.config_manager import ConfigManager @@ -1352,7 +1354,9 @@ def _on_timestamp_granularities_change(self) -> None: :return: None """ # Dictionary mapping checkboxes to their corresponding file types - chk_to_timestamp_granularity = { + chk_to_timestamp_granularity: dict[ + ctk.CTkCheckBox, TimestampGranularitiesType + ] = { self.chk_timestamp_granularities_segment: TimestampGranularities.SEGMENT.value, self.chk_timestamp_granularities_word: TimestampGranularities.WORD.value, } @@ -1369,7 +1373,7 @@ def _on_timestamp_granularities_change(self) -> None: selected_timestamp_granularities ) self._config_whisper_api.timestamp_granularities = ( - selected_timestamp_granularities_str + selected_timestamp_granularities ) # Notify the config change From e982a435a6f357a42ce3e23ab18913f6031e53d8 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:29:49 +0200 Subject: [PATCH 66/81] Modify the logic of `_on_output_file_types_change` to match `_on_timestamp_granularities` --- src/views/main_window.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/views/main_window.py b/src/views/main_window.py index 15fd180..171e69b 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -13,6 +13,7 @@ ConfigWhisperApi, TimestampGranularitiesType, ) +from models.config.config_whisperx import ConfigWhisperX, OutputFileTypes from models.transcription import Transcription from PIL import Image from utils.config_manager import ConfigManager @@ -24,6 +25,7 @@ TimestampGranularities, TranscriptionMethod, WhisperApiResponseFormats, + WhisperXFileTypes, ) from utils.env_keys import EnvKeys @@ -862,9 +864,7 @@ def _get_transcription_properties(self) -> dict[str, Any]: properties["should_translate"] = bool( self.chk_whisper_options_translate.get() ) - properties[ - "output_file_types" - ] = self._config_whisperx.output_file_types.split(",") + properties["output_file_types"] = self._config_whisperx.output_file_types return properties @@ -1270,35 +1270,35 @@ def _on_output_file_types_change(self) -> None: :return: None """ # Dictionary mapping checkboxes to their corresponding file types - checkbox_to_file_type = { - self.chk_output_file_vtt: "vtt", - self.chk_output_file_srt: "srt", - self.chk_output_file_txt: "txt", - self.chk_output_file_json: "json", - self.chk_output_file_tsv: "tsv", - self.chk_output_file_aud: "aud", + chk_to_output_file_type: dict[ctk.CTkCheckBox, OutputFileTypes] = { + self.chk_output_file_aud: WhisperXFileTypes.AUD.value, + self.chk_output_file_json: WhisperXFileTypes.JSON.value, + self.chk_output_file_srt: WhisperXFileTypes.SRT.value, + self.chk_output_file_tsv: WhisperXFileTypes.TSV.value, + self.chk_output_file_txt: WhisperXFileTypes.TXT.value, + self.chk_output_file_vtt: WhisperXFileTypes.VTT.value, } # List comprehension to gather selected file types - output_file_types = [ - file_type for chk, file_type in checkbox_to_file_type.items() if chk.get() + selected_output_file_types = [ + file_type for chk, file_type in chk_to_output_file_type.items() if chk.get() ] # Show or hide the subtitle options frame based on the selected subtitle file types - if any(file_type in output_file_types for file_type in {"vtt", "srt"}): + if any(file_type in selected_output_file_types for file_type in {"vtt", "srt"}): self.frm_subtitle_options.grid() else: self.frm_subtitle_options.grid_remove() # Convert the list to a comma-separated string and update the configuration - output_file_types_str = ",".join(output_file_types) - self._config_whisperx.output_file_types = output_file_types_str + selected_output_file_types_str = ",".join(selected_output_file_types) + self._config_whisperx.output_file_types = selected_output_file_types # Notify the config change self._on_config_change( section=ConfigWhisperX.Key.SECTION, key=ConfigWhisperX.Key.OUTPUT_FILE_TYPES, - new_value=output_file_types_str, + new_value=selected_output_file_types_str, ) def _toggle_chk_timestamp_granularities(self) -> None: From cd125bf405d3230a6d5d1eb87803abb1921c8835 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:31:43 +0200 Subject: [PATCH 67/81] Only pass `timestamp_granularities` as a parameter to `client.audio.transcriptions.create` if it has a value to fix the type error --- src/handlers/openai_api_handler.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/handlers/openai_api_handler.py b/src/handlers/openai_api_handler.py index 4a55817..d7b6d4a 100644 --- a/src/handlers/openai_api_handler.py +++ b/src/handlers/openai_api_handler.py @@ -19,7 +19,7 @@ def transcribe(audio_data: sr.AudioData, transcription: Transcription) -> str: config = cm.ConfigManager.get_config_whisper_api() compressed_audio = AudioHandler.compress_audio(audio_data) timestamp_granularities = ( - config.timestamp_granularities.split(",") + config.timestamp_granularities if config.response_format == WhisperApiResponseFormats.VERBOSE_JSON.value else None ) @@ -28,14 +28,23 @@ def transcribe(audio_data: sr.AudioData, transcription: Transcription) -> str: api_key=EnvKeys.OPENAI_API_KEY.get_value(), timeout=120.0 # 2 minutes ) - whisper_api_transcription = client.audio.transcriptions.create( - model="whisper-1", - file=compressed_audio, - language=transcription.language_code, - response_format=config.response_format, - temperature=config.temperature, - timestamp_granularities=timestamp_granularities, - ) + if timestamp_granularities: + whisper_api_transcription = client.audio.transcriptions.create( + model="whisper-1", + file=compressed_audio, + language=transcription.language_code, + response_format=config.response_format, + temperature=config.temperature, + timestamp_granularities=timestamp_granularities, + ) + else: + whisper_api_transcription = client.audio.transcriptions.create( + model="whisper-1", + file=compressed_audio, + language=transcription.language_code, + response_format=config.response_format, + temperature=config.temperature, + ) if WhisperApiResponseFormats.JSON.value in config.response_format: return whisper_api_transcription.to_json() From 5ad2e920d3c746ef205dd9cb3624bb5a2b4e35c5 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:32:18 +0200 Subject: [PATCH 68/81] Add missing `OutputFileTypes` to the `config_whisper_x` file --- src/models/config/config_whisperx.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/models/config/config_whisperx.py b/src/models/config/config_whisperx.py index d9d82cc..cca3377 100644 --- a/src/models/config/config_whisperx.py +++ b/src/models/config/config_whisperx.py @@ -2,6 +2,8 @@ from enum import Enum from typing import Literal +OutputFileTypes = Literal["aud", "json", "srt", "tsv", "txt", "vtt"] + @dataclass class ConfigWhisperX: @@ -10,7 +12,7 @@ class ConfigWhisperX: compute_type: str use_cpu: bool can_use_gpu: bool - output_file_types: str + output_file_types: list[OutputFileTypes] class Key(Enum): """ @@ -38,7 +40,7 @@ def value_type(self) -> str: ConfigWhisperX.Key.COMPUTE_TYPE: "str", ConfigWhisperX.Key.USE_CPU: "bool", ConfigWhisperX.Key.CAN_USE_GPU: "bool", - ConfigWhisperX.Key.OUTPUT_FILE_TYPES: "str", + ConfigWhisperX.Key.OUTPUT_FILE_TYPES: "list", } return str(type_mapping.get(self)) From 678c19483e45b1b7bf34fda33b2ae6ce4f979f76 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 14:39:12 +0200 Subject: [PATCH 69/81] Add `pre-commit` package to `requirements-dev.txt` --- requirements-dev.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements-dev.txt b/requirements-dev.txt index 776bb0d..f51f88c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1 +1,2 @@ mypy==1.11.0 +pre-commit==3.7.1 From 9f17db2d661f8463dd32b1be26812dfa38e46fce Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 16:12:23 +0200 Subject: [PATCH 70/81] Add `.pre-commit-config.yaml` file --- .pre-commit-config.yaml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e31d7db --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,28 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.6.0 + hooks: + - id: check-ast + - id: check-case-conflict + - id: check-docstring-first + - id: check-merge-conflict + - id: check-vcs-permalinks + - id: debug-statements + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: requirements-txt-fixer + - id: no-commit-to-branch + args: [ --branch, main ] + - id: trailing-whitespace + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.10.0 + hooks: + - id: mypy + args: ["--strict"] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.5.5 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + - id: ruff-format From 9e29bee7d2384e2774564cac7f82e5bef7201061 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 16:24:43 +0200 Subject: [PATCH 71/81] Add the pre-commit.ci status badge to the `README` and mark the pre-commit task of the Roadmap as completed --- README.md | 205 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 106 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index 0b07d13..7c16fcf 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,16 @@
- - Logo @@ -29,53 +29,60 @@

Audiotext

A desktop application that transcribes audio from files, microphone input or YouTube videos with the option to translate the content and create subtitles.

+ + pre-commit.ci status + +
- Version - GitHub Contributors - License
- GitHub Contributors - Issues - GitHub pull requests

Report Bug - - · + + · Request Feature - - · + + · Ask Question @@ -94,7 +101,7 @@ - [Getting Started](#getting-started) - [Installation](#installation) - [Set Up the Project Locally](#set-up-the-project-locally) - - [Notes](#notes) + - [Notes](#notes) - [Usage](#usage) - [Transcription Language](#transcription-language) - [Transcription Method](#transcription-method) @@ -370,7 +377,7 @@ You can also choose the theme you like best. It can be dark, light, or the one c ├───models │ │ __init__.py │ │ transcription.py - │ │ + │ │ │ └───config │ __init__.py │ config_subtitles.py @@ -390,9 +397,9 @@ You can also choose the theme you like best. It can be dark, light, or the one c │ path_helper.py │ └───views - │ __init__.py + │ __init__.py │ main_window.py - │ + │ └───custom_widgets __init__.py ctk_scrollable_dropdown/ @@ -426,7 +433,7 @@ You can also choose the theme you like best. It can be dark, light, or the one c ### Installation -1. Install [FFmpeg](https://ffmpeg.org) to execute the program. Otherwise, it won't be able to process the audio files. +1. Install [FFmpeg](https://ffmpeg.org) to execute the program. Otherwise, it won't be able to process the audio files. To check if you have it installed on your system, run `ffmpeg -version`. It should return something similar to this: ``` @@ -446,16 +453,16 @@ You can also choose the theme you like best. It can be dark, light, or the one c ``` # on Ubuntu or Debian sudo apt update && sudo apt install ffmpeg - + # on Arch Linux sudo pacman -S ffmpeg - + # on MacOS using Homebrew (https://brew.sh/) brew install ffmpeg - + # on Windows using Chocolatey (https://chocolatey.org/) choco install ffmpeg - + # on Windows using Scoop (https://scoop.sh/) scoop install ffmpeg ``` @@ -463,7 +470,7 @@ You can also choose the theme you like best. It can be dark, light, or the one c 3. Decompress the downloaded file. 4. Open the `audiotext` folder and double-click the `Audiotext` executable file. -### Set Up the Project Locally +### Set Up the Project Locally 1. Clone the repository by running `git clone https://github.com/HenestrosaDev/audiotext.git`. 2. Change the current working directory to `audiotext` by running `cd audiotext`. 3. (Optional but recommended) Create a Python virtual environment in the project root. If you're using `virtualenv`, you would run `virtualenv venv`. @@ -474,7 +481,7 @@ You can also choose the theme you like best. It can be dark, light, or the one c # if you get the error `FullyQualifiedErrorId : UnauthorizedAccess`, run this: Set-ExecutionPolicy Unrestricted -Scope Process # and then . venv/Scripts/activate - + # on macOS and Linux source venv/Scripts/activate ``` @@ -496,15 +503,15 @@ You can also choose the theme you like best. It can be dark, light, or the one c Once you open the **Audiotext** executable file (explained in the [Getting Started](#getting-started) section), you'll see something like this: - - - Main @@ -535,7 +542,7 @@ There are three transcription methods available in **Audiotext**: You can transcribe from four different audio sources: -- **File** (see image above): Click the file explorer icon to select the file you want to transcribe, or manually enter the path to the file in the `Path` input field. You can transcribe audio from both audio and video files. +- **File** (see image above): Click the file explorer icon to select the file you want to transcribe, or manually enter the path to the file in the `Path` input field. You can transcribe audio from both audio and video files. Note that the file explorer has the `All supported files` option selected by default. To select only audio files or video files, click the combo box in the lower right corner of the file explorer to change the file type, as marked in red in the following image: @@ -543,18 +550,18 @@ You can transcribe from four different audio sources: ![Supported files](docs/supported-files.png) -- **Directory**: Click the file explorer icon to select the directory containing the files you want to transcribe, or manually enter the path to the directory in the `Path` input field. Note that the `Autosave` option is checked and cannot be unchecked because each file's transcription will automatically be saved in the same path as the source file. +- **Directory**: Click the file explorer icon to select the directory containing the files you want to transcribe, or manually enter the path to the directory in the `Path` input field. Note that the `Autosave` option is checked and cannot be unchecked because each file's transcription will automatically be saved in the same path as the source file. - - - Main @@ -596,7 +603,7 @@ You can transcribe from four different audio sources: Note that if we check the `Overwrite existing files` option, all files will be processed again and the existing transcription files will be overwritten. -- **Microphone**: To start recording, simply click the `Start recording` button to begin the process. The text of the button will change to `Stop recording` and its color will change to red. Click it to stop recording and generate the transcription. +- **Microphone**: To start recording, simply click the `Start recording` button to begin the process. The text of the button will change to `Stop recording` and its color will change to red. Click it to stop recording and generate the transcription. Here is a video demonstrating this feature: @@ -604,19 +611,19 @@ You can transcribe from four different audio sources: https://github.com/user-attachments/assets/61f2173b-bcfb-4251-a910-0cf6b37598c6 Note that your operating system must recognize an input source, otherwise an error message will appear in the text box indicating that no input source was detected. - + - **YouTube video**: Requires an Internet connection to get the audio of the video. To generate the transcription, simply enter the URL of the video in the `YouTube video URL` field and click the `Generate transcription` button when you are finished adjusting the settings. - - - From microphone @@ -632,7 +639,7 @@ If checked, the transcription will automatically be saved in the root of the fol Note that if you create a transcription using the `Microphone` or `YouTube` audio sources with the `Autosave` action enabled, the transcription files will be saved in the root of the `audiotext-vX.X.X` directory. -#### Overwrite Existing Files +#### Overwrite Existing Files This option can only be checked if the `Autosave` option is checked. If `Overwrite existing files` is checked, existing transcriptions in the root directory of the file to be transcribed will be overwritten when saving. @@ -655,12 +662,12 @@ The `Google API options` frame appears if the selected transcription method is *

- - google-api-options @@ -673,12 +680,12 @@ Since the program uses the free **Google API** tier by default, which allows you

- - Google API key dialog @@ -693,12 +700,12 @@ The `Whisper API options` frame appears if the selected transcription method is

- - Whisper API options @@ -713,12 +720,12 @@ To add it, click the `Set OpenAI API key` button. You'll be presented with a dia

- - OpenAI API key dialog @@ -729,11 +736,11 @@ OpenAI charges for the use of the API key, for which **Audiotext** is not respon #### Response Format -The format of the transcript output, in one of these options: +The format of the transcript output, in one of these options: -- `json` +- `json` - `srt` (subtitle file type) -- `text` +- `text` - `verbose_json` - `vtt` (subtitle file type) @@ -747,7 +754,7 @@ Defaults to 0. #### Timestamp Granularities -The timestamp granularities to populate for this transcription. `Response format` must be set `verbose_json` to use timestamp granularities. Either or both of these options are supported: `word`, or `segment`. +The timestamp granularities to populate for this transcription. `Response format` must be set `verbose_json` to use timestamp granularities. Either or both of these options are supported: `word`, or `segment`. **Note**: There is no additional latency for segment timestamps, but generating word timestamps incurs additional latency. @@ -759,16 +766,16 @@ The **WhisperX** options appear when the selected transcription method is **Whis

- - - WhisperX options @@ -776,7 +783,7 @@ The **WhisperX** options appear when the selected transcription method is **Whis #### Output File Types -You can select one or more of the following transcription output file types: +You can select one or more of the following transcription output file types: - `.aud` - `.json` @@ -802,16 +809,16 @@ When you select the `.srt` and/or the `.vtt` output file type(s), the `Subtitle

- - - Subtitle options @@ -827,7 +834,7 @@ Underline each word as it's spoken in `.srt` and `.vtt` subtitle files. Not chec The maximum number of lines in a segment. `2` by default. -#### Max. Line Width +#### Max. Line Width The maximum number of characters in a line before breaking the line. `42` by default. @@ -837,12 +844,12 @@ When you click the `Show advanced options` button in the `WhisperX options` fram

- - WhisperX advanced options @@ -881,8 +888,8 @@ This term refers to different data types used in computing, particularly in the There are three possible values for **Audiotext**: - `int8`: Default if using CPU. It represents whole numbers without any fractional part. Its size is 8 bits (1 byte) and it can represent integer values from -128 to 127 (signed) or 0 to 255 (unsigned). It is used in scenarios where memory efficiency is critical, such as in quantized neural networks or edge devices with limited computational resources. - `float16`: Default if using CUDA GPU. It's a half precision type representing 16-bit floating point numbers. Its size is 16 bits (2 bytes). It has a smaller range and precision compared to `float32`. It's often used in applications where memory is a critical resource, such as in deep learning models running on GPUs or TPUs. -- `float32`: Recommended for CUDA GPUs with more than 8 GB of VRAM. It's a single precision type representing 32-bit floating point numbers, which is a standard for representing real numbers in computers. Its size is 32 bits (4 bytes). It can represent a wide range of real numbers with a reasonable level of precision. - +- `float32`: Recommended for CUDA GPUs with more than 8 GB of VRAM. It's a single precision type representing 32-bit floating point numbers, which is a standard for representing real numbers in computers. Its size is 32 bits (4 bytes). It can represent a wide range of real numbers with a reasonable level of precision. + #### Batch Size This option determines how many samples are processed together before the model parameters are updated. It doesn't affect the quality of the transcription, only the generation speed (the smaller, the slower). @@ -896,7 +903,7 @@ For simplicity, let's divide the possible batch size values into two groups: **WhisperX** will use the CPU for transcription if checked. Checked by default if there is no CUDA GPU. -As noted in the [Compute Type](#compute-type) section, the default compute type value for the CPU is `int8`, since many CPUs don't support efficient `float16` or `float32` computation, which would result in an error. Change it at your own risk. +As noted in the [Compute Type](#compute-type) section, the default compute type value for the CPU is `int8`, since many CPUs don't support efficient `float16` or `float32` computation, which would result in an error. Change it at your own risk. ## Troubleshooting @@ -927,7 +934,7 @@ You'll be prompted with an error like this: ``` RateLimitError("Error code: 429 - {'error': {'message': 'You exceeded your current quota, please check your plan and billing details. For more information on this error, read the docs: https://platform.openai.com/docs/guides/error-codes/api-errors.', 'type': 'insufficient_quota', 'param': None, 'code': 'insufficient_quota'}}") -``` +``` This is either because your account run out of credits or because you need to fund your account before you can use the API for the first time (even if you have free credits available). To fix this, you need to purchase credits for your account (starting at $5) with a credit or debit card by going to the [Billing](https://platform.openai.com/settings/organization/billing/overview) section of your OpenAI account settings. @@ -954,9 +961,9 @@ If you are using an API key that was created before you funded your account for - [x] Add support for `.json`, `.tsv` and `.aud` output file types when using WhisperX as transcription method. - [x] Add `appearance_mode` to `config.ini`. - [x] Add support for **Whisper's API** ([#42](https://github.com/HenestrosaDev/audiotext/discussions/42)). +- [x] Add pre-commit configuration for using `ruff` and `mypy`. - [ ] Change the `Generate transcription` button to `Cancel transcription` when a transcription is in progress. - [ ] Generate executables for macOS and Linux. -- [ ] Add pre-commit config for using `Black`, `isort`, and `mypy`. - [ ] Add tests. You can propose a new feature creating an [issue](https://github.com/HenestrosaDev/audiotext/issues/new/choose). @@ -971,7 +978,7 @@ See also the list of [contributors](https://github.com/HenestrosaDev/audiotext/c -## Contributing +## Contributing Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please read the [CONTRIBUTING.md](https://github.com/HenestrosaDev/audiotext/blob/main/.github/CONTRIBUTING.md) file, where you can find more detailed information about how to contribute to the project. From c5a98526e5f255b664e6aa17942f023dc5f06205 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 16:30:11 +0200 Subject: [PATCH 72/81] Add steps to the "Setting Up the Project Locally" section to set up the pre-commit hooks --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7c16fcf..847c19a 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ - [Built With](#built-with) - [Getting Started](#getting-started) - [Installation](#installation) - - [Set Up the Project Locally](#set-up-the-project-locally) + - [Setting Up the Project Locally](#setting-up-the-project-locally) - [Notes](#notes) - [Usage](#usage) - [Transcription Language](#transcription-language) @@ -470,7 +470,8 @@ You can also choose the theme you like best. It can be dark, light, or the one c 3. Decompress the downloaded file. 4. Open the `audiotext` folder and double-click the `Audiotext` executable file. -### Set Up the Project Locally +### Setting Up the Project Locally + 1. Clone the repository by running `git clone https://github.com/HenestrosaDev/audiotext.git`. 2. Change the current working directory to `audiotext` by running `cd audiotext`. 3. (Optional but recommended) Create a Python virtual environment in the project root. If you're using `virtualenv`, you would run `virtualenv venv`. @@ -486,10 +487,13 @@ You can also choose the theme you like best. It can be dark, light, or the one c source venv/Scripts/activate ``` 5. Run `pip install -r requirements.txt` to install the dependencies. -6. Copy and paste the `.env.example` file as `.env` to the root of the directory. -7. Run `python src/app.py` to start the program. +6. (Optional) If you want to contribute to the project, run `pip install -r requirements-dev.txt` to install the development dependencies. +7. (Optional) If you followed step 6, run `pre-commit` to install the pre-commit hooks in your `.git/` directory. +8. Copy and paste the `.env.example` file as `.env` to the root of the directory. +9. Run `python src/app.py` to start the program. ### Notes + - You cannot generate a single executable file for this project with PyInstaller due to the dependency with the CustomTkinter package (reason [here](https://github.com/TomSchimansky/CustomTkinter/wiki/Packaging)). - For **Apple Silicon Macs**: An error occurs when trying to install the `pyaudio` package. [Here](https://stackoverflow.com/questions/73268630/error-could-not-build-wheels-for-pyaudio-which-is-required-to-install-pyprojec) is a StackOverflow post explaining how to solve this issue. - I had to comment out the lines `pprint(response_text, indent=4)` in the `recognize_google` function from the `__init__.py` file of the `SpeechRecognition` package to avoid opening a command line along with the GUI. Otherwise, the program would not be able to use the Google API transcription method because `pprint` throws an error if it cannot print to the CLI, preventing the code from generating the transcription. The same applies to the lines using the `logger` package in the `moviepy/audio/io/ffmpeg_audiowriter` file from the `moviepy` package. There is also a change in the line 169 that changes `logger=logger` to `logger=None` to avoid more errors related to opening the console. From f9152f9a433e0b67c5ff4289169655ed6a12ebbe Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:32:17 +0000 Subject: [PATCH 73/81] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .env.example | 2 +- .github/CONTRIBUTING.md | 4 ++-- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/bug_report_template.md | 6 +++--- .../feature_request_template.md | 4 ++-- .../pull_request_template.md | 3 --- .gitignore | 2 +- LICENSE | 10 +++++----- config.ini | 1 - requirements.txt | Bin 564 -> 539 bytes res/locales/es/LC_MESSAGES/main_window.po | Bin 3550 -> 3551 bytes src/handlers/openai_api_handler.py | 3 ++- src/views/main_window.py | 9 +++------ 13 files changed, 20 insertions(+), 26 deletions(-) diff --git a/.env.example b/.env.example index 3c60636..62e35f5 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,2 @@ GOOGLE_API_KEY= -OPENAI_API_KEY= \ No newline at end of file +OPENAI_API_KEY= diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5061027..4967eed 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -112,9 +112,9 @@ This section guides you through submitting an enhancement suggestion for audiote #### How Do I Submit a Good Enhancement Suggestion? -Enhancement suggestions are tracked as [GitHub issues](https://github.com/HenestrosaConH/audiotext/issues). This is the [issue template](https://github.com/HenestrosaConH/audiotext/tree/main/.github/workflows/ISSUE_TEMPLATE.md). +Enhancement suggestions are tracked as [GitHub issues](https://github.com/HenestrosaConH/audiotext/issues). This is the [issue template](https://github.com/HenestrosaConH/audiotext/tree/main/.github/workflows/ISSUE_TEMPLATE.md). -Don't forget to follow these principles: +Don't forget to follow these principles: - Use a **clear and descriptive title** for the issue to identify the suggestion. - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4a384cc..825743b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -ko_fi: henestrosadev \ No newline at end of file +ko_fi: henestrosadev diff --git a/.github/ISSUE_TEMPLATE/bug_report_template.md b/.github/ISSUE_TEMPLATE/bug_report_template.md index 8be2840..44e991f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_template.md +++ b/.github/ISSUE_TEMPLATE/bug_report_template.md @@ -1,5 +1,5 @@ --- -name: Bug Report +name: Bug Report about: Create a report to help us improve title: "[Bug] " labels: '' @@ -34,7 +34,7 @@ System information - **System**: (For example, "Ubuntu 20.04 LTS x64", "Windows 11 x64", or "macOS Monterey") - **System language**: (Please, indicate the region as well) -- **Audiotext version**: +- **Audiotext version**: Code To Duplicate @@ -55,4 +55,4 @@ Screenshot, Sketch, or Drawing Optional. Feel free to provide an image if you think it adds more useful information to the issue. -You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. \ No newline at end of file +You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. diff --git a/.github/ISSUE_TEMPLATE/feature_request_template.md b/.github/ISSUE_TEMPLATE/feature_request_template.md index aa813bc..7db1791 100644 --- a/.github/ISSUE_TEMPLATE/feature_request_template.md +++ b/.github/ISSUE_TEMPLATE/feature_request_template.md @@ -12,7 +12,7 @@ Overview In this section, provide a brief overview of the enhancement you are proposing. -Motivation +Motivation ------------------ In this section, describe the motivation behind the enhancement. What problem are you trying to solve? How will this enhancement benefit users of the project? @@ -37,4 +37,4 @@ Next Steps In this section, outline the next steps for implementing the enhancement. This could include assigning the enhancement to a specific team member, setting a timeline for implementation, or opening a pull request for review. -An enhancement template provides a clear and structured approach for proposing enhancements to a project, which can help ensure that proposals are well-thought-out and considered by the project's maintainers. \ No newline at end of file +An enhancement template provides a clear and structured approach for proposing enhancements to a project, which can help ensure that proposals are well-thought-out and considered by the project's maintainers. diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md index 65ee279..21d4089 100644 --- a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -28,6 +28,3 @@ Put an x in the boxes that apply. ## Additional Notes Please provide any additional information or context about your changes here. - - - diff --git a/.gitignore b/.gitignore index 177316a..11ec53f 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,4 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -audio-chunks/ \ No newline at end of file +audio-chunks/ diff --git a/LICENSE b/LICENSE index b6cd659..4e754be 100644 --- a/LICENSE +++ b/LICENSE @@ -19,9 +19,9 @@ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/config.ini b/config.ini index a7b67e4..40cdfbe 100644 --- a/config.ini +++ b/config.ini @@ -25,4 +25,3 @@ compute_type = float16 use_cpu = False can_use_gpu = False output_file_types = txt - diff --git a/requirements.txt b/requirements.txt index 7a16eec16c403fc134d423299f40777bb32f8f36..df2a01735c314c4790ccff80c8fba9a205098ae3 100644 GIT binary patch delta 154 zcmdnOGMi;$xwbVILoP!;Lm5LRLn=c7LnVVP5E?S*F&F@`F#{J+t^g>L$B+nA0h2Rk zFaxQW_*hYz3#=)Hp_CyBEDh3T#9#^}4JRuzYEKSeRGM7EXe7zSPy#fnh#{FF1FX&n atPZ5=Fr(_^SBx5yMVJ&Axh51basdFH^&GeW delta 100 zcmbQuvV~>x3dSNvXa5w?L$P~-uTi9i<_0M&qKkp0uS z6iqpGz4s+rF%3ske0TOO+#kotu@(jXN)if|Ryti}w+POmy;O zMq5VS$rl-y0?BfwXdro+DH2HPGS3E*Cz+=M$wU@iAlc6X67OdbpTueeWEiu$vw{re zo&1p7lGTjC2*^_8v1K*^(tlYsfhyG5EKyWgLRFXo>BnsDKo#2TNM=mlz|H~`+sbYa zB)_uTBk8qZFazo-=gJXSRz^$U5VK`M9@;VP_IjTwxg_80=`WL|TiirKvO IETAv~0J5!D$^ZZW diff --git a/src/handlers/openai_api_handler.py b/src/handlers/openai_api_handler.py index d7b6d4a..0111bfe 100644 --- a/src/handlers/openai_api_handler.py +++ b/src/handlers/openai_api_handler.py @@ -25,7 +25,8 @@ def transcribe(audio_data: sr.AudioData, transcription: Transcription) -> str: ) client = OpenAI( - api_key=EnvKeys.OPENAI_API_KEY.get_value(), timeout=120.0 # 2 minutes + api_key=EnvKeys.OPENAI_API_KEY.get_value(), + timeout=120.0, # 2 minutes ) if timestamp_granularities: diff --git a/src/views/main_window.py b/src/views/main_window.py index 171e69b..7603e44 100644 --- a/src/views/main_window.py +++ b/src/views/main_window.py @@ -1405,12 +1405,9 @@ def _toggle_frm_subtitle_options_visibility(self) -> None: :return: None """ - if ( - self._config_transcription.method == TranscriptionMethod.WHISPERX.value - and ( - "srt" in self._config_whisperx.output_file_types - or "vtt" in self._config_whisperx.output_file_types - ) + if self._config_transcription.method == TranscriptionMethod.WHISPERX.value and ( + "srt" in self._config_whisperx.output_file_types + or "vtt" in self._config_whisperx.output_file_types ): if "srt" in self._config_whisperx.output_file_types: self.chk_output_file_srt.select() From 279dbeedbe181863293ed34f05483c2e47dc9adb Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 17:19:09 +0200 Subject: [PATCH 74/81] Add `additional_dependencies` to the `mypy` hook to avoid `import-not-found` errors on CI --- .pre-commit-config.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e31d7db..3bca880 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,19 @@ repos: hooks: - id: mypy args: ["--strict"] + additional_dependencies: + - customtkinter==5.2.1 + - moviepy==1.0.3 + - openai==1.36.0 + - pyaudio==0.2.13 + - pydub==0.25.1 + - python-dotenv==1.0.1 + - pytubefix==6.5.2 + - SpeechRecognition==3.9.0 + - torch==2.2.1 + - torchaudio==2.2.1 + - torchvision==0.17.1 + - whisperx==3.1.5 - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.5 hooks: From b5cdc348265e58e757b530038cb86570ce98ce3e Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 18:05:20 +0200 Subject: [PATCH 75/81] Run `mypy` from local installation instead of from the remote repo --- .pre-commit-config.yaml | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3bca880..9d32c77 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,24 +15,14 @@ repos: - id: no-commit-to-branch args: [ --branch, main ] - id: trailing-whitespace - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.0 + - repo: local hooks: - - id: mypy - args: ["--strict"] - additional_dependencies: - - customtkinter==5.2.1 - - moviepy==1.0.3 - - openai==1.36.0 - - pyaudio==0.2.13 - - pydub==0.25.1 - - python-dotenv==1.0.1 - - pytubefix==6.5.2 - - SpeechRecognition==3.9.0 - - torch==2.2.1 - - torchaudio==2.2.1 - - torchvision==0.17.1 - - whisperx==3.1.5 + - id: mypy-local + name: Run mypy with all dev dependencies present + language: system + types: + - python + entry: mypy --strict - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.5 hooks: From e068ef91abfe6b03b543be7ec7eedb69a9766b9c Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 18:07:10 +0200 Subject: [PATCH 76/81] Remove the pre-commit.ci status badge from the `README` --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 847c19a..2a55242 100644 --- a/README.md +++ b/README.md @@ -29,13 +29,6 @@

Audiotext

A desktop application that transcribes audio from files, microphone input or YouTube videos with the option to translate the content and create subtitles.

- - pre-commit.ci status - -
Date: Tue, 30 Jul 2024 19:55:28 +0200 Subject: [PATCH 77/81] Add `Code Quality` workflow --- .github/workflows/code-quality.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .github/workflows/code-quality.yml diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml new file mode 100644 index 0000000..9788b6e --- /dev/null +++ b/.github/workflows/code-quality.yml @@ -0,0 +1,30 @@ +name: Code Quality + +on: + pull_request: + branches: + - "*" + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.10] + steps: + - uses: actions/checkout@master + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt -r requirements-dev.txt + - name: Run pre-commit + run: | + pre-commit run --all-files --show-diff-on-failure From 6744818d0827ec7d62e11b76631feeef5d3c9a95 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 19:56:25 +0200 Subject: [PATCH 78/81] Add "Set up a CI pipeline to apply the pre-commit hooks" to the Roadmap --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2a55242..37fd264 100644 --- a/README.md +++ b/README.md @@ -959,6 +959,7 @@ If you are using an API key that was created before you funded your account for - [x] Add `appearance_mode` to `config.ini`. - [x] Add support for **Whisper's API** ([#42](https://github.com/HenestrosaDev/audiotext/discussions/42)). - [x] Add pre-commit configuration for using `ruff` and `mypy`. +- [x] Set up a CI pipeline to apply the pre-commit hooks. - [ ] Change the `Generate transcription` button to `Cancel transcription` when a transcription is in progress. - [ ] Generate executables for macOS and Linux. - [ ] Add tests. From a3717116d2a6cbad4fcba15e0cb9e84c6db1ab40 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 19:58:25 +0200 Subject: [PATCH 79/81] Fix value of `python-version` in `code-quality.yml` --- .github/workflows/code-quality.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 9788b6e..b3fb77d 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.10] + python-version: ["3.10"] steps: - uses: actions/checkout@master - name: Set up Python ${{ matrix.python-version }} From b1ac94f66f3f6e8429033f0cee33ed6725d806dd Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 20:04:40 +0200 Subject: [PATCH 80/81] Change encoding of `requirements.txt` to UTF-8 without BOM --- requirements.txt | Bin 539 -> 269 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/requirements.txt b/requirements.txt index df2a01735c314c4790ccff80c8fba9a205098ae3..37fd2f2ee2a276c17d94086d5a6f9d3ae7df3321 100644 GIT binary patch literal 269 zcmYk0OK!t33`F-jMK565vfvagz)Q3za3nS&>WG3Y`E&ayN*7(e0cRe&j*gt>j)SA4 zD`|Q2Imh+0@15*nk=a2!vn1<5(yib9(%Yg(t&}ZmmV5r?kSQTfFoITh*_h&qs9^x3 zBvP39N#+8BisxJ$3*zuRHkDnuHkxX;dc1_rNv5#97Ft-Y^uq_@1*$`RMjOM#udhH$kOjK}?RlH1tzo#*Sf$%wWKmJ$)PVMZ zyKYi;wG|ui=L^i<_BA$YOI@cNRv-B~vdli#Iy0@3^n%l8Fq~oPBhyo)Omw}~Bdj%Q z8+3PQI$0ML6&3gF1@4~A5PZ8i@jQ<+^0%}9IiB3%!eh#Ohn|xk$z$9z98S5N!c$x- z`}xjkJpqisHTV8ZO}xn&+n`ogNQ|sKG7q|Dc~nPuEoaSBaT?{^P!X+dVA{Ur^v_Vz HAd!9nVpCU< From 4fd00b13108a1ad876e8bf666011bd3acd058c78 Mon Sep 17 00:00:00 2001 From: HenestrosaDev Date: Tue, 30 Jul 2024 20:12:43 +0200 Subject: [PATCH 81/81] Add step in `code-quality.yml` to install PortAudio, necessary for PyAudio installation --- .github/workflows/code-quality.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index b3fb77d..4afd0a1 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -21,6 +21,10 @@ jobs: with: python-version: ${{ matrix.python-version }} cache: 'pip' + - name: Install PortAudio to install PyAudio + run: | + sudo apt-get update + sudo apt-get install python3-pyaudio portaudio19-dev python3-dev - name: Install dependencies run: | python -m pip install --upgrade pip