diff --git a/.gitignore b/.gitignore index d71a95df..97e5bfc4 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ diarize_x86_64_gernophil diarize_x86_64_gernophil_unsigned diarize_x86_64_mod diarize_x86_64_mod_unsigned + +.python-version diff --git a/diarize.py b/diarize.py index 34231393..1ccc7e49 100644 --- a/diarize.py +++ b/diarize.py @@ -73,7 +73,7 @@ def __call__( if platform.system() == 'Windows': pipeline = Pipeline.from_pretrained(os.path.join(app_dir, 'models', 'pyannote_config.yaml')) pipeline.to(torch.device(device)) - elif platform.system() == "Darwin": # = MAC + elif platform.system() in ("Darwin", "Linux"): # = MAC if device == 'mps' and not torch.backends.mps.is_available(): # should only happen on x86_64, but checked on all archs to be sure device = 'cpu' print("log: 'pyannote_xpu: mps' was selected, but mps is not available on this system!") diff --git a/environments/requirements-linux.in b/environments/requirements-linux.in new file mode 100644 index 00000000..0c117328 --- /dev/null +++ b/environments/requirements-linux.in @@ -0,0 +1,10 @@ +customtkinter +Pillow +PyYAML +appdirs +AdvancedHTMLParser +elevate +python-i18n +pyannote.audio @ git+https://github.com/kaixxx/pyannote-audio.git +faster-whisper +pyinstaller \ No newline at end of file diff --git a/environments/requirements-linux.txt b/environments/requirements-linux.txt new file mode 100644 index 00000000..88d1f75c --- /dev/null +++ b/environments/requirements-linux.txt @@ -0,0 +1,430 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --output-file=requirements-linux.txt requirements-linux.in +# +advancedhtmlparser==9.0.2 + # via -r requirements-linux.in +aiohttp==3.8.6 + # via fsspec +aiosignal==1.3.1 + # via aiohttp +alembic==1.12.1 + # via optuna +altgraph==0.17.4 + # via pyinstaller +antlr4-python3-runtime==4.9.3 + # via omegaconf +appdirs==1.4.4 + # via -r requirements-linux.in +asteroid-filterbanks==0.4.0 + # via pyannote-audio +async-timeout==4.0.3 + # via aiohttp +attrs==23.1.0 + # via aiohttp +audioread==3.0.1 + # via librosa +av==10.0.0 + # via faster-whisper +certifi==2023.7.22 + # via requests +cffi==1.16.0 + # via soundfile +charset-normalizer==3.3.2 + # via + # aiohttp + # requests +click==8.1.7 + # via typer +colorama==0.4.6 + # via typer +coloredlogs==15.0.1 + # via onnxruntime +colorlog==6.7.0 + # via optuna +contourpy==1.2.0 + # via matplotlib +ctranslate2==3.20.0 + # via faster-whisper +customtkinter==5.2.1 + # via -r requirements-linux.in +cycler==0.12.1 + # via matplotlib +darkdetect==0.8.0 + # via customtkinter +decorator==5.1.1 + # via librosa +docopt==0.6.2 + # via + # pyannote-metrics + # pyannote-pipeline +einops==0.7.0 + # via pyannote-audio +elevate==0.1.3 + # via -r requirements-linux.in +faster-whisper==0.9.0 + # via -r requirements-linux.in +filelock==3.13.1 + # via + # huggingface-hub + # pyannote-pipeline + # torch + # triton +flatbuffers==23.5.26 + # via onnxruntime +fonttools==4.44.0 + # via matplotlib +frozenlist==1.4.0 + # via + # aiohttp + # aiosignal +fsspec[http]==2023.10.0 + # via + # huggingface-hub + # lightning + # pytorch-lightning + # torch +greenlet==3.0.1 + # via sqlalchemy +huggingface-hub==0.17.3 + # via + # faster-whisper + # pyannote-audio + # speechbrain + # tokenizers +humanfriendly==10.0 + # via coloredlogs +hyperpyyaml==1.2.2 + # via speechbrain +idna==3.4 + # via + # requests + # yarl +importlib-metadata==6.8.0 + # via pyinstaller +importlib-resources==6.1.1 + # via matplotlib +jinja2==3.1.2 + # via torch +joblib==1.3.2 + # via + # librosa + # scikit-learn + # speechbrain +julius==0.2.7 + # via torch-audiomentations +kiwisolver==1.4.5 + # via matplotlib +lazy-loader==0.3 + # via librosa +librosa==0.10.1 + # via torch-audiomentations +lightning==2.1.0 + # via pyannote-audio +lightning-utilities==0.9.0 + # via + # lightning + # pytorch-lightning + # torchmetrics +llvmlite==0.41.1 + # via numba +mako==1.2.4 + # via alembic +markdown-it-py==3.0.0 + # via rich +markupsafe==2.1.3 + # via + # jinja2 + # mako +matplotlib==3.8.1 + # via pyannote-metrics +mdurl==0.1.2 + # via markdown-it-py +mpmath==1.3.0 + # via sympy +msgpack==1.0.7 + # via librosa +multidict==6.0.4 + # via + # aiohttp + # yarl +networkx==3.2.1 + # via torch +numba==0.58.1 + # via librosa +numpy==1.26.1 + # via + # asteroid-filterbanks + # contourpy + # ctranslate2 + # librosa + # lightning + # matplotlib + # numba + # onnxruntime + # optuna + # pandas + # pyannote-core + # pyannote-metrics + # pytorch-lightning + # pytorch-metric-learning + # scikit-learn + # scipy + # soxr + # speechbrain + # tensorboardx + # torchmetrics +nvidia-cublas-cu12==12.1.3.1 + # via + # nvidia-cudnn-cu12 + # nvidia-cusolver-cu12 + # torch +nvidia-cuda-cupti-cu12==12.1.105 + # via torch +nvidia-cuda-nvrtc-cu12==12.1.105 + # via torch +nvidia-cuda-runtime-cu12==12.1.105 + # via torch +nvidia-cudnn-cu12==8.9.2.26 + # via torch +nvidia-cufft-cu12==11.0.2.54 + # via torch +nvidia-curand-cu12==10.3.2.106 + # via torch +nvidia-cusolver-cu12==11.4.5.107 + # via torch +nvidia-cusparse-cu12==12.1.0.106 + # via + # nvidia-cusolver-cu12 + # torch +nvidia-nccl-cu12==2.18.1 + # via torch +nvidia-nvjitlink-cu12==12.3.52 + # via + # nvidia-cusolver-cu12 + # nvidia-cusparse-cu12 +nvidia-nvtx-cu12==12.1.105 + # via torch +omegaconf==2.3.0 + # via pyannote-audio +onnxruntime==1.16.1 + # via faster-whisper +optuna==3.4.0 + # via pyannote-pipeline +packaging==23.2 + # via + # huggingface-hub + # lightning + # lightning-utilities + # matplotlib + # onnxruntime + # optuna + # pooch + # pyinstaller + # pytorch-lightning + # speechbrain + # tensorboardx + # torch-pitch-shift +pandas==2.1.2 + # via + # pyannote-database + # pyannote-metrics +pillow==10.1.0 + # via + # -r requirements-linux.in + # matplotlib +platformdirs==3.11.0 + # via pooch +pooch==1.8.0 + # via librosa +primepy==1.3 + # via torch-pitch-shift +protobuf==4.25.0 + # via + # onnxruntime + # tensorboardx +pyannote-audio @ git+https://github.com/kaixxx/pyannote-audio.git + # via -r requirements-linux.in +pyannote-core==5.0.0 + # via + # pyannote-audio + # pyannote-database + # pyannote-metrics + # pyannote-pipeline +pyannote-database==5.0.1 + # via + # pyannote-audio + # pyannote-metrics + # pyannote-pipeline +pyannote-metrics==3.2.1 + # via pyannote-audio +pyannote-pipeline==3.0.1 + # via pyannote-audio +pycparser==2.21 + # via cffi +pygments==2.16.1 + # via rich +pyinstaller==6.1.0 + # via -r requirements-linux.in +pyinstaller-hooks-contrib==2023.10 + # via pyinstaller +pyparsing==3.1.1 + # via matplotlib +python-dateutil==2.8.2 + # via + # matplotlib + # pandas +python-i18n==0.3.9 + # via -r requirements-linux.in +pytorch-lightning==2.1.0 + # via lightning +pytorch-metric-learning==2.3.0 + # via pyannote-audio +pytz==2023.3.post1 + # via pandas +pyyaml==6.0.1 + # via + # -r requirements-linux.in + # ctranslate2 + # huggingface-hub + # hyperpyyaml + # lightning + # omegaconf + # optuna + # pyannote-database + # pyannote-pipeline + # pytorch-lightning +queryablelist==3.1.0 + # via advancedhtmlparser +requests==2.31.0 + # via + # fsspec + # huggingface-hub + # pooch +rich==13.6.0 + # via + # pyannote-audio + # typer +ruamel-yaml==0.18.5 + # via hyperpyyaml +ruamel-yaml-clib==0.2.8 + # via ruamel-yaml +scikit-learn==1.3.2 + # via + # librosa + # pyannote-metrics + # pyannote-pipeline + # pytorch-metric-learning +scipy==1.11.3 + # via + # librosa + # pyannote-core + # pyannote-metrics + # scikit-learn + # speechbrain +semver==3.0.2 + # via pyannote-audio +sentencepiece==0.1.99 + # via speechbrain +shellingham==1.5.4 + # via typer +six==1.16.0 + # via python-dateutil +sortedcontainers==2.4.0 + # via pyannote-core +soundfile==0.12.1 + # via + # librosa + # pyannote-audio +soxr==0.3.7 + # via librosa +speechbrain==0.5.15 + # via pyannote-audio +sqlalchemy==2.0.23 + # via + # alembic + # optuna +sympy==1.12 + # via + # onnxruntime + # pyannote-metrics + # torch +tabulate==0.9.0 + # via pyannote-metrics +tensorboardx==2.6.2.2 + # via pyannote-audio +threadpoolctl==3.2.0 + # via scikit-learn +tokenizers==0.14.1 + # via faster-whisper +torch==2.1.0 + # via + # asteroid-filterbanks + # julius + # lightning + # pyannote-audio + # pytorch-lightning + # pytorch-metric-learning + # speechbrain + # torch-audiomentations + # torch-pitch-shift + # torchaudio + # torchmetrics +torch-audiomentations==0.11.0 + # via pyannote-audio +torch-pitch-shift==1.2.4 + # via torch-audiomentations +torchaudio==2.1.0 + # via + # pyannote-audio + # speechbrain + # torch-audiomentations + # torch-pitch-shift +torchmetrics==1.2.0 + # via + # lightning + # pyannote-audio + # pytorch-lightning +tqdm==4.66.1 + # via + # huggingface-hub + # lightning + # optuna + # pyannote-pipeline + # pytorch-lightning + # pytorch-metric-learning + # speechbrain +triton==2.1.0 + # via torch +typer[all]==0.9.0 + # via + # pyannote-database + # typer +typing-extensions==4.8.0 + # via + # alembic + # asteroid-filterbanks + # huggingface-hub + # librosa + # lightning + # lightning-utilities + # pyannote-core + # pytorch-lightning + # sqlalchemy + # torch + # typer +tzdata==2023.3 + # via pandas +urllib3==2.0.7 + # via requests +yarl==1.9.2 + # via aiohttp +zipp==3.17.0 + # via + # importlib-metadata + # importlib-resources + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/ffmpeg-linux-x86_64 b/ffmpeg-linux-x86_64 new file mode 100755 index 00000000..f4450e77 Binary files /dev/null and b/ffmpeg-linux-x86_64 differ diff --git a/noScribe-linux.spec b/noScribe-linux.spec new file mode 100644 index 00000000..8e70d6c6 --- /dev/null +++ b/noScribe-linux.spec @@ -0,0 +1,57 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_data_files +from PyInstaller.utils.hooks import copy_metadata + +datas = [('models/faster-whisper-large-v2', 'models/faster-whisper-large-v2/'), ('models/faster-whisper-small', 'models/faster-whisper-small/'), ('noScribeEdit', 'noScribeEdit/'), ('trans', 'trans/'), ('graphic_sw.png', '.'), ('LICENSE.txt', '.'), ('noScribeLogo.ico', '.'), ('prompt.yml', '.'), ('README.md', '.')] +datas += collect_data_files('customtkinter') +datas += copy_metadata('AdvancedHTMLParser') + + +block_cipher = None + + +a = Analysis( + ['noScribe.py'], + pathex=[], + binaries=[('ffmpeg-linux-x86_64', '.')], + datas=datas, + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='noScribe', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon=['noScribeLogo.ico'], +) +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='noScribe', +) diff --git a/noScribe.py b/noScribe.py index bc7e54d9..63336bb4 100644 --- a/noScribe.py +++ b/noScribe.py @@ -51,8 +51,9 @@ from pathlib import Path if platform.system() == 'Windows': import cpufeature -if platform.system() == "Darwin": # = MAC +if platform.system() in ("Darwin", "Linux"): # = macOS or Linux import shlex +if platform.system() == 'Darwin': import Foundation import logging @@ -129,6 +130,9 @@ def save_config(): # determine optimal number of threads for faster-whisper (depending on cpu cores) if platform.system() == 'Windows': number_threads = cpufeature.CPUFeature["num_physical_cores"] +elif platform.system() == "Linux": + number_threads = os.cpu_count() + number_threads = 4 if number_threads is None else number_threads elif platform.system() == "Darwin": # = MAC if platform.machine() == "arm64": number_threads = int(int(check_output(["sysctl", @@ -214,11 +218,14 @@ def __init__(self): # configure window self.title('noScribe - ' + t('app_header')) - if platform.system() == "Darwin": + if platform.system() in ("Darwin", "Linux"): self.geometry(f"{1100}x{695}") else: self.geometry(f"{1100}x{650}") - self.iconbitmap('noScribeLogo.ico') + if platform.system() in ("Darwin", "Windows"): + self.iconbitmap('noScribeLogo.ico') + if platform.system() == "Linux": + self.iconphoto(True, tk.PhotoImage(file='noScribeLogo.png')) # header self.frame_header = ctk.CTkFrame(self, height=100) @@ -415,10 +422,13 @@ def launch_editor(self, file): # Launch the editor in a seperate process so that in can stay running even if noScribe quits. # Source: https://stackoverflow.com/questions/13243807/popen-waiting-for-child-process-even-when-the-immediate-child-has-terminated/13256908#13256908 # set system/version dependent "start_new_session" analogs + program: str = None if platform.system() == 'Windows': program = os.path.join(app_dir, 'noScribeEdit', 'noScribeEdit.exe') elif platform.system() == "Darwin": # = MAC program = os.path.join(os.sep, 'Applications', 'noScribeEdit.app', 'Contents', 'MacOS', 'noScribeEdit') + elif platform.system() == "Linux": + program = os.path.join(app_dir, 'noScribeEdit', "noScribeEdit") kwargs = {} if platform.system() == 'Windows': # from msdn [1] @@ -428,10 +438,11 @@ def launch_editor(self, file): else: # should work on all POSIX systems, Linux and macOS kwargs.update(start_new_session=True) - # p = Popen(["C"], stdin=PIPE, stdout=PIPE, stderr=PIPE, **kwargs) - p = Popen([program, file], **kwargs) - # assert not p.poll() - + if program is not None and os.path.exists(program): + Popen([program, file], **kwargs) + else: + self.logn(t('err_noScribeEdit_not_found'), 'error') + def openLink(self, link): if link.startswith('file://') and link.endswith('.html'): self.launch_editor(link[7:]) @@ -471,7 +482,7 @@ def reader_thread(self, q): def button_audio_file_event(self): fn = tk.filedialog.askopenfilename(initialdir=os.path.dirname(self.audio_file), initialfile=os.path.basename(self.audio_file)) - if fn != '': + if fn: self.audio_file = fn self.logn(t('log_audio_file_selected') + self.audio_file) self.button_audio_file_name.configure(text=os.path.basename(self.audio_file)) @@ -485,7 +496,7 @@ def button_transcript_file_event(self): _initialfile = Path(os.path.basename(self.audio_file)).stem fn = tk.filedialog.asksaveasfilename(initialdir=_initialdir, initialfile=_initialfile, filetypes=[('noScribe Transcript','*.html')], defaultextension='html') - if fn != '': + if fn: self.transcript_file = fn self.logn(t('log_transcript_filename') + self.transcript_file) self.button_transcript_file_name.configure(text=os.path.basename(self.transcript_file)) @@ -749,6 +760,11 @@ def transcription_worker(self): ffmpeg_abspath = os.path.join(app_dir, 'ffmpeg') ffmpeg_cmd = f'{ffmpeg_abspath} -nostdin -loglevel warning -y -ss {self.start}ms {end_pos_cmd} -i \"{self.audio_file}\" -ar 16000 -ac 1 -c:a pcm_s16le {self.tmp_audio_file}' ffmpeg_cmd = shlex.split(ffmpeg_cmd) + elif platform.system() == "Linux": + # TODO: Use system ffmpeg if available + ffmpeg_abspath = os.path.join(app_dir, 'ffmpeg-linux-x86_64') + ffmpeg_cmd = f'{ffmpeg_abspath} -nostdin -loglevel warning -y -ss {self.start}ms {end_pos_cmd} -i \"{self.audio_file}\" -ar 16000 -ac 1 -c:a pcm_s16le {self.tmp_audio_file}' + ffmpeg_cmd = shlex.split(ffmpeg_cmd) else: raise Exception('Platform not supported yet.') self.logn(ffmpeg_cmd, where='file') @@ -760,7 +776,7 @@ def transcription_worker(self): with Popen(ffmpeg_cmd, stdout=PIPE, stderr=STDOUT, bufsize=1,universal_newlines=True,encoding='utf-8', startupinfo=startupinfo) as ffmpeg_proc: for line in ffmpeg_proc.stdout: self.logn('ffmpeg: ' + line) - elif platform.system() == "Darwin": # = MAC + elif platform.system() in ("Darwin", "Linux"): with Popen(ffmpeg_cmd, stdout=PIPE, stderr=STDOUT, bufsize=1,universal_newlines=True,encoding='utf-8') as ffmpeg_proc: for line in ffmpeg_proc.stdout: self.logn('ffmpeg: ' + line) @@ -850,7 +866,7 @@ def find_speaker(diarization, transcript_start, transcript_end): elif platform.system() == 'Darwin': # = MAC # No check for arm64 or x86_64 necessary, since the correct version will be compiled and bundled diarize_abspath = os.path.join(app_dir, '..', 'MacOS', 'diarize') - if not os.path.exists(diarize_abspath): # Run the compiled version of diarize if it exists, otherwise the python script: + if not 'diarize_abspath' in globals() or not os.path.exists(diarize_abspath): # Run the compiled version of diarize if it exists, otherwise the python script: diarize_abspath = 'python ' + os.path.join(app_dir, 'diarize.py') diarize_cmd = f'{diarize_abspath} {self.pyannote_xpu} "{self.tmp_audio_file}" "{diarize_output}"' diarize_env = None @@ -863,7 +879,7 @@ def find_speaker(diarization, transcript_start, transcript_end): # (supresses the terminal, see: https://stackoverflow.com/questions/1813872/running-a-process-in-pythonw-with-popen-without-a-console) startupinfo = STARTUPINFO() startupinfo.dwFlags |= STARTF_USESHOWWINDOW - elif platform.system() == 'Darwin': # = MAC + elif platform.system() in ('Darwin', "Linux"): # = MAC diarize_cmd = shlex.split(diarize_cmd) startupinfo = None else: @@ -1006,7 +1022,7 @@ def save_doc(): from faster_whisper import WhisperModel if platform.system() == 'Windows': whisper_device = 'cpu' - elif platform.system() == "Darwin": # = MAC + elif platform.system() in ("Darwin", "Linux"): # = MAC or Linux whisper_device = 'auto' else: raise Exception('Platform not supported yet.') @@ -1270,4 +1286,4 @@ def on_closing(self): app = App() - app.mainloop() \ No newline at end of file + app.mainloop() diff --git a/trans/noScribe.de.yml b/trans/noScribe.de.yml index 587a213b..1204e1bf 100644 --- a/trans/noScribe.de.yml +++ b/trans/noScribe.de.yml @@ -69,6 +69,7 @@ de: err_transcription: 'Fehler im 3. Schritt - Transkription' rescue_saving: 'Beim Speichern unter dem ursprünglichen Dateinamen ist ein Fehler aufgetreten. Stattdessen wurde das Transkript in dieser Datei gesichert: %{file}' rescue_saving_failed: 'Fehler: Beim Speichern unter dem ursprünglichen Dateinamen ist ein Fehler aufgetreten. Der alternative Dateiname existiert ebenso bereits. Die Transkription wird beendet. (Das Transkript darf nicht bereits in Word geöffnet sein!)' + err_noScribeEdit_not_found: 'Fehler: Der noScribe Editor konnte nicht gefunden werden.' # Doc contents doc_header: 'Transkribiert mit noScribe Vers. %{version}' diff --git a/trans/noScribe.en.yml b/trans/noScribe.en.yml index 1f9644d0..4de46f84 100644 --- a/trans/noScribe.en.yml +++ b/trans/noScribe.en.yml @@ -72,6 +72,7 @@ en: err_transcription: 'Error in step 3 - transcription' rescue_saving: 'Saving to the original filename failed. Saved instead in this file: ' rescue_saving_failed: 'Error: Saving to the original filename failed. The alternate filename also exists. I have to quit. (Make sure your transcript is not already opened in Word!)' + err_noScribeEdit_not_found: 'Error: Could not find the noScribe Editor.' # Doc contents doc_header: 'Transcribed with noScribe vers. %{version}'