Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the necessary changes to compile the program for macOS #59

Merged
merged 9 commits into from
Sep 4, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
202 changes: 99 additions & 103 deletions audiotext.spec
Original file line number Diff line number Diff line change
@@ -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=[],
Expand All @@ -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.",
}
)
2 changes: 1 addition & 1 deletion config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ appearance_mode = System
language = English
audio_source = File
method = WhisperX
autosave = True
autosave = False
overwrite_files = False

[whisper_api]
Expand Down
Binary file removed res/img/icon.icns
Binary file not shown.
15 changes: 15 additions & 0 deletions res/macos/entitlements.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- These are required for binaries built by PyInstaller -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
</dict>
</plist>
Binary file added res/macos/icon.icns
Binary file not shown.
File renamed without changes.
6 changes: 4 additions & 2 deletions src/app.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/handlers/whisperx_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
5 changes: 3 additions & 2 deletions src/utils/env_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
14 changes: 11 additions & 3 deletions src/utils/path_helper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import sys
from pathlib import Path

Expand All @@ -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"
Expand Down
Loading