diff --git a/README.md b/README.md index 72c32f8..d98392e 100644 --- a/README.md +++ b/README.md @@ -639,7 +639,7 @@ When you click on the `Save transcription` button, you'll be prompted for a file #### Autosave -If checked, the transcription will automatically be saved in the root of the folder where the file to transcribe is stored. If there are already existing files with the same name, they won't be overwritten. To do that, you'll need to check the `Overwrite existing files` option (see below). +Unchecked by default. If checked, the transcription will automatically be saved in the root of the folder where the file to transcribe is stored. If there are already existing files with the same name, they won't be overwritten. To do that, you'll need to check the `Overwrite existing files` option (see below). 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. diff --git a/audiotext.spec b/audiotext.spec index 25e8705..9bb08aa 100644 --- a/audiotext.spec +++ b/audiotext.spec @@ -1,46 +1,65 @@ # -*- mode: python ; coding: utf-8 -*- -from os.path import join -from platform import system -from PyInstaller.utils.hooks import copy_metadata -from PyInstaller.utils.hooks import collect_data_files -from shutil import copyfile +from pathlib import Path +from PyInstaller.compat import is_darwin, is_win +import shutil + +import sys ; sys.setrecursionlimit(sys.getrecursionlimit() * 5) + +def find_site_packages(venv_dir = "venv"): + venv_path = Path(venv_dir) + for site_packages in venv_path.rglob("site-packages"): + if site_packages.is_dir(): + return site_packages + + return None + +site_packages_path = find_site_packages() datas = [ - (r'venv/Lib/site-packages/customtkinter', 'customtkinter'), - (r'venv/Lib/site-packages/transformers', 'transformers'), - (r'venv/Lib/site-packages/lightning', 'lightning'), - (r'venv/Lib/site-packages/lightning_fabric', 'lightning_fabric'), - (r'venv/Lib/site-packages/speechbrain', 'speechbrain'), - (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'), - ('config.ini', '.'), - ('.env', '.'), + (f"{site_packages_path}/customtkinter", "customtkinter"), + (f"{site_packages_path}/transformers", "transformers"), + (f"{site_packages_path}/lightning", "lightning"), + (f"{site_packages_path}/lightning_fabric", "lightning_fabric"), + (f"{site_packages_path}/speechbrain", "speechbrain"), + (f"{site_packages_path}/pyannote", "pyannote"), + (f"{site_packages_path}/asteroid_filterbanks", "asteroid_filterbanks"), + (f"{site_packages_path}/whisperx", "whisperx"), + (f"{site_packages_path}/librosa", "librosa"), + ("res", "res"), + ("config.ini", "."), + (".env", "."), ] -datas += copy_metadata('torch') -datas += copy_metadata('tqdm', recursive=True) -datas += copy_metadata('regex') -datas += copy_metadata('requests') -datas += copy_metadata('packaging') -datas += copy_metadata('filelock') -datas += copy_metadata('numpy') -datas += copy_metadata('tokenizers') -datas += copy_metadata('pillow') -datas += copy_metadata('huggingface_hub') -datas += copy_metadata('safetensors') -datas += copy_metadata('pyyaml') -datas += collect_data_files('librosa') +hiddenimports = [ + "huggingface_hub.repository", + "sklearn.utils._cython_blas", + "sklearn.neighbors.quad_tree", + "sklearn.tree", + "sklearn.tree._utils", +] block_cipher = None +is_debug = False +if is_debug: + options = [("v", None, "OPTION")] +else: + options = [] + +binaries = [ + (shutil.which("ffmpeg"), "."), + (shutil.which("ffprobe"), "."), +] + +if flac_path := shutil.which("flac"): + binaries += [(flac_path, ".")] + a = Analysis( - ['src/app.py'], - pathex=[], - binaries=[], + ["src/app.py"], + pathex=[site_packages_path], + binaries=binaries, datas=datas, - hiddenimports=['huggingface_hub.repository', 'pytorch', 'sklearn.utils._cython_blas', 'sklearn.neighbors.typedefs', 'sklearn.neighbors.quad_tree', 'sklearn.tree', 'sklearn.tree._utils'], + hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -51,76 +70,53 @@ a = Analysis( noarchive=False, ) -# Filter out unused and/or duplicate shared libs -torch_lib_paths = { - join('torch', 'lib', 'libtorch_cuda.so'), - join('torch', 'lib', 'libtorch_cpu.so'), -} -a.datas = [entry for entry in a.datas if not entry[0] in torch_lib_paths] +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) -os_path_separator = '\\' if system() == 'Windows' else '/' -a.datas = [entry for entry in a.datas if not f'torch{os_path_separator}_C.cp' in entry[0]] -a.datas = [entry for entry in a.datas if not f'torch{os_path_separator}_dl.cp' in entry[0]] +macos_icon = "res/macos/icon.icns" +windows_icon = "res/windows/icon.ico" -pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +exe = EXE( + pyz, + a.scripts, + options, + exclude_binaries=True, + name="Audiotext", + debug=is_debug, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=is_debug, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file="res/macos/entitlements.plist" if is_darwin else None, + icon=windows_icon if is_win else macos_icon, +) -if system() == 'Darwin': # macOS - exe = EXE( - pyz, - a.scripts, - a.binaries, - a.zipfiles, - a.datas, - [], - name='Audiotext', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - upx_exclude=[], - runtime_tmpdir=None, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch='x86_64', - codesign_identity=None, - entitlements_file=None, - icon=['res/img/icon.icns'], - ) - - # BUNDLE statement is used to create a macOS application bundle (.app) for the program - app = BUNDLE( - exe, - name='Audiotext.app', - icon=['res/img/icon.icns'], - bundle_identifier=None, - ) -else: - exe = EXE( - pyz, - a.scripts, - [], - exclude_binaries=True, - name='Audiotext', - debug=False, - bootloader_ignore_signals=False, - strip=False, - upx=True, - console=False, - disable_windowed_traceback=False, - argv_emulation=False, - target_arch='x86_64', - codesign_identity=None, - entitlements_file=None, - icon=['res/img/icon.ico'], - ) - coll = COLLECT( - exe, - a.binaries, - a.zipfiles, - a.datas, - strip=False, - upx=True, - upx_exclude=[], - name='audiotext', - ) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name="Audiotext", +) + +app = BUNDLE( + coll, + name="Audiotext.app", + icon=macos_icon, + bundle_identifier="com.henestrosadev.audiotext", + version="2.3.0", + info_plist={ + "NSPrincipalClass": "NSApplication", + "NSAppleScriptEnabed": False, + "NSHighResolutionCapable": True, + "NSMicrophoneUsageDescription": "Allow Audiotext to record audio from your microphone to generate transcriptions.", + } +) diff --git a/config.ini b/config.ini index 40cdfbe..2c40455 100644 --- a/config.ini +++ b/config.ini @@ -10,7 +10,7 @@ appearance_mode = System language = English audio_source = File method = WhisperX -autosave = True +autosave = False overwrite_files = False [whisper_api] diff --git a/res/img/icon.icns b/res/img/icon.icns deleted file mode 100644 index 64dbf57..0000000 Binary files a/res/img/icon.icns and /dev/null differ diff --git a/res/macos/entitlements.plist b/res/macos/entitlements.plist new file mode 100644 index 0000000..279586c --- /dev/null +++ b/res/macos/entitlements.plist @@ -0,0 +1,15 @@ + + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-library-validation + + com.apple.security.device.audio-input + + + diff --git a/res/macos/icon.icns b/res/macos/icon.icns new file mode 100644 index 0000000..4cd2478 Binary files /dev/null and b/res/macos/icon.icns differ diff --git a/res/img/icon.ico b/res/windows/icon.ico similarity index 100% rename from res/img/icon.ico rename to res/windows/icon.ico diff --git a/src/app.py b/src/app.py index 468b3ed..1e9d51e 100644 --- a/src/app.py +++ b/src/app.py @@ -1,5 +1,4 @@ import customtkinter as ctk -import torch import utils.config_manager as cm import utils.constants as c import utils.path_helper as ph @@ -23,7 +22,7 @@ def __init__(self) -> None: ctk.set_default_color_theme("blue") self.title(c.APP_NAME) - self.wm_iconbitmap(ph.ROOT_PATH / ph.IMG_RELATIVE_PATH / "icon.ico") + self.wm_iconbitmap(ph.ROOT_PATH / "res/windows/icon.ico") # Initial size of the window width = 1000 @@ -35,6 +34,9 @@ def __init__(self) -> None: min_height = 250 self.minsize(min_width, min_height) + # Place the torch import here to avoid the "No ffmpeg exe could be found" error + import torch + # Check GPU cm.ConfigManager.modify_value( section=ConfigWhisperX.Key.SECTION, diff --git a/src/handlers/whisperx_handler.py b/src/handlers/whisperx_handler.py index d608b59..485cb66 100644 --- a/src/handlers/whisperx_handler.py +++ b/src/handlers/whisperx_handler.py @@ -112,6 +112,7 @@ 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" # type: ignore[index] + if self._whisperx_result: + self._whisperx_result["language"] = "en" writer(self._whisperx_result, file_path, vars(config_subtitles)) diff --git a/src/utils/env_keys.py b/src/utils/env_keys.py index 731b66d..3155aaf 100644 --- a/src/utils/env_keys.py +++ b/src/utils/env_keys.py @@ -2,9 +2,10 @@ from enum import Enum from typing import Optional -from dotenv import find_dotenv, load_dotenv +import utils.path_helper as ph +from dotenv import load_dotenv -load_dotenv(find_dotenv()) +load_dotenv(ph.ROOT_PATH / ".env") class EnvKeys(Enum): diff --git a/src/utils/path_helper.py b/src/utils/path_helper.py index 872ee8c..4f52dc2 100644 --- a/src/utils/path_helper.py +++ b/src/utils/path_helper.py @@ -1,3 +1,4 @@ +import os import sys from pathlib import Path @@ -8,13 +9,20 @@ def get_root_path() -> Path: Taken from the [PyInstaller docs](https://pyinstaller.org/en/stable/runtime-information.html) - :return: The absolute path to the file or directory specified by the relative path. + :return: The absolute path to the program directory. :rtype: Path """ if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): - return Path(sys._MEIPASS) + root_path = Path(sys._MEIPASS) else: - return Path(__file__).parent.parent.parent + root_path = Path(__file__).parent.parent.parent + + # Add the root path to the PATH in order to allow macOS to pick up the binaries, + # since `.app` bundles launched from Finder get a very limited shell environment. + # https://github.com/orgs/pyinstaller/discussions/8773 + os.environ["PATH"] += os.pathsep + str(root_path) + + return root_path IMG_RELATIVE_PATH = "res/img"