From 05ff5765bf7c9d8173ab8a05c8cd530f59155bc9 Mon Sep 17 00:00:00 2001 From: Vitali Graf Date: Sat, 11 Apr 2026 12:01:02 +0200 Subject: [PATCH] feat: Windows-Packaging mit PyInstaller (ZIP ohne Python-Installation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fügt Build-Infrastruktur hinzu, mit der whisper-local als selbständiges Windows-ZIP-Paket ohne Python-Installation bereitgestellt werden kann. - whisper_local.spec: PyInstaller onedir-Konfiguration für Windows 64-bit mit allen nativen DLLs (ctranslate2/CUDA, pywin32, PortAudio, onnxruntime, av/FFmpeg) und Hidden Imports für platform-bedingte Backends - build.ps1: Build-Skript das versioniertes ZIP erstellt (.\build.ps1 -Clean) - transcriber.py: portabler Modell-Cache neben der EXE im gebündelten Modus - pyproject.toml: pyinstaller>=6.0 als [build]-Abhängigkeitsgruppe, v1.0.0 Co-Authored-By: Claude Sonnet 4.6 --- build.ps1 | 75 ++++++++++++++++ pyproject.toml | 5 +- uv.lock | 86 +++++++++++++++++- whisper_local.spec | 170 +++++++++++++++++++++++++++++++++++ whisper_local/transcriber.py | 17 +++- 5 files changed, 350 insertions(+), 3 deletions(-) create mode 100644 build.ps1 create mode 100644 whisper_local.spec diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..96c7f4f --- /dev/null +++ b/build.ps1 @@ -0,0 +1,75 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Baut whisper-local mit PyInstaller und erstellt ein versioniertes ZIP. +.DESCRIPTION + Liest die Version aus pyproject.toml, führt PyInstaller aus und packt + das dist-Verzeichnis als whisper-local-v{version}-win64.zip. +.EXAMPLE + .\build.ps1 + .\build.ps1 -Clean # löscht dist/ und build/ vor dem Build + .\build.ps1 -SkipBuild # nur ZIP aus bestehendem dist/ erstellen +#> +param( + [switch]$Clean, + [switch]$SkipBuild +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# ── Versionsnummer aus pyproject.toml lesen ─────────────────────────────────── +$pyproject = Get-Content "pyproject.toml" -Raw +if ($pyproject -match 'version\s*=\s*"([^"]+)"') { + $version = $Matches[1] +} else { + Write-Error "Konnte Version nicht aus pyproject.toml lesen." + exit 1 +} + +$zipName = "whisper-local-v$version-win64.zip" +$distDir = "dist\whisper-local" + +Write-Host "=== whisper-local Build v$version ===" -ForegroundColor Cyan + +# ── Optional: Bereinigen ────────────────────────────────────────────────────── +if ($Clean) { + Write-Host "Bereinige build/ und dist/ ..." -ForegroundColor Yellow + if (Test-Path "build") { Remove-Item -Recurse -Force "build" } + if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" } + if (Test-Path $zipName) { Remove-Item -Force $zipName } +} + +# ── PyInstaller ausführen ──────────────────────────────────────────────────── +if (-not $SkipBuild) { + Write-Host "Synchronisiere Build-Abhängigkeiten ..." -ForegroundColor Yellow + uv sync --group build + + Write-Host "Starte PyInstaller ..." -ForegroundColor Yellow + uv run --group build python -m PyInstaller whisper_local.spec --noconfirm + + if ($LASTEXITCODE -ne 0) { + Write-Error "PyInstaller fehlgeschlagen (Exit-Code $LASTEXITCODE)" + exit $LASTEXITCODE + } +} + +# ── Prüfen ob dist-Ordner vorhanden ────────────────────────────────────────── +if (-not (Test-Path $distDir)) { + Write-Error "Ordner '$distDir' nicht gefunden — Build fehlgeschlagen?" + exit 1 +} + +# ── ZIP erstellen ───────────────────────────────────────────────────────────── +if (Test-Path $zipName) { Remove-Item -Force $zipName } + +Write-Host "Erstelle $zipName ..." -ForegroundColor Yellow +Compress-Archive -Path $distDir -DestinationPath $zipName -CompressionLevel Optimal + +$sizeMB = [math]::Round((Get-Item $zipName).Length / 1MB, 1) +Write-Host "" +Write-Host "Fertig: $zipName ($sizeMB MB)" -ForegroundColor Green +Write-Host "" +Write-Host "Testen:" -ForegroundColor Cyan +Write-Host " Expand-Archive $zipName -DestinationPath test-release" +Write-Host " .\test-release\whisper-local\whisper-local.exe" diff --git a/pyproject.toml b/pyproject.toml index 886853c..1d5c0f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "whisper-local" -version = "0.1.0" +version = "1.0.0" requires-python = ">=3.13" dependencies = [ "faster-whisper>=1.1.0", @@ -27,3 +27,6 @@ dev = [ "pytest>=8.0.0", "pytest-asyncio>=0.24.0", ] +build = [ + "pyinstaller>=6.0", +] diff --git a/uv.lock b/uv.lock index df7907f..d8e175d 100644 --- a/uv.lock +++ b/uv.lock @@ -2,6 +2,15 @@ version = 1 revision = 3 requires-python = ">=3.13" +[[package]] +name = "altgraph" +version = "0.17.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -313,6 +322,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "macholib" +version = "1.16.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -430,6 +451,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "pefile" +version = "2024.8.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" }, +] + [[package]] name = "pillow" version = "12.2.0" @@ -498,6 +528,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyinstaller" +version = "6.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "altgraph" }, + { name = "macholib", marker = "sys_platform == 'darwin'" }, + { name = "packaging" }, + { name = "pefile", marker = "sys_platform == 'win32'" }, + { name = "pyinstaller-hooks-contrib" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/63/fd62472b6371d89dc138d40c36d87a50dc2de18a035803bbdc376b4ffac4/pyinstaller-6.19.0.tar.gz", hash = "sha256:ec73aeb8bd9b7f2f1240d328a4542e90b3c6e6fbc106014778431c616592a865", size = 4036072, upload-time = "2026-02-14T18:06:28.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/eb/23374721fecfa72677e79800921cb6aceefa6ba48574dc404f3f6c6c3be7/pyinstaller-6.19.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4190e76b74f0c4b5c5f11ac360928cd2e36ec8e3194d437bf6b8648c7bc0c134", size = 1040563, upload-time = "2026-02-14T18:05:22.436Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7e/dfd724b0b533f5aaec0ee5df406fe2319987ed6964480a706f85478b12ea/pyinstaller-6.19.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8bd68abd812d8a6ba33b9f1810e91fee0f325969733721b78151f0065319ca11", size = 735477, upload-time = "2026-02-14T18:05:27.143Z" }, + { url = "https://files.pythonhosted.org/packages/88/c9/ee3a4101c31f26344e66896c73c1fd6ed8282bf871473365b7f8674af406/pyinstaller-6.19.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1ec54ef967996ca61dacba676227e2b23219878ccce5ee9d6f3aada7b8ed8abf", size = 747143, upload-time = "2026-02-14T18:05:31.488Z" }, + { url = "https://files.pythonhosted.org/packages/da/0a/fc77e9f861be8cf300ac37155f59cc92aff99b29f2ddd78546f563a5b5a6/pyinstaller-6.19.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4ab2bb52e58448e14ddf9450601bdedd66800465043501c1d8f1cab87b60b122", size = 744849, upload-time = "2026-02-14T18:05:35.492Z" }, + { url = "https://files.pythonhosted.org/packages/6d/e3/6872e020ee758afe0b821663858492c10745608b07150e5e2c824a5b3e1c/pyinstaller-6.19.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:da6d5c6391ccefe73554b9fa29b86001c8e378e0f20c2a4004f836ba537eff63", size = 741590, upload-time = "2026-02-14T18:05:39.59Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/b8db5f1a4b0fb228175f2ea0aa33f949adcc097fbe981cc524f9faf85777/pyinstaller-6.19.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a0fc5f6b3c55aa54353f0c74ffa59b1115433c1850c6f655d62b461a2ed6cbbe", size = 741448, upload-time = "2026-02-14T18:05:45.636Z" }, + { url = "https://files.pythonhosted.org/packages/6f/4d/63b0600f2694e9141b83129fbc1c488ec84d5a0770b1448ec154dcd0fee9/pyinstaller-6.19.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:e649ba6bd1b0b89b210ad92adb5fbdc8a42dd2c5ca4f72ef3a0bfec83a424b83", size = 740613, upload-time = "2026-02-14T18:05:49.726Z" }, + { url = "https://files.pythonhosted.org/packages/01/d4/e812ad36178093a0e9fd4b8127577748dd85b0cb71de912229dca21fd741/pyinstaller-6.19.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:481a909c8e60c8692fc60fcb1344d984b44b943f8bc9682f2fcdae305ad297e6", size = 740350, upload-time = "2026-02-14T18:05:54.093Z" }, + { url = "https://files.pythonhosted.org/packages/52/03/b2c2ee41fb8e10fd2a45d21f5ec2ef25852cfb978dbf762972eed59e3d63/pyinstaller-6.19.0-py3-none-win32.whl", hash = "sha256:3c5c251054fe4cfaa04c34a363dcfbf811545438cb7198304cd444756bc2edd2", size = 1324317, upload-time = "2026-02-14T18:06:00.085Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d3/6d5e62b8270e2b53a6065e281b3a7785079b00e9019c8019952828dd1669/pyinstaller-6.19.0-py3-none-win_amd64.whl", hash = "sha256:b5bb6536c6560330d364d91522250f254b107cf69129d9cbcd0e6727c570be33", size = 1384894, upload-time = "2026-02-14T18:06:06.425Z" }, + { url = "https://files.pythonhosted.org/packages/81/65/458cd523308a101a22fd2742893405030cc24994cc74b1b767cecf137160/pyinstaller-6.19.0-py3-none-win_arm64.whl", hash = "sha256:c2d5a539b0bfe6159d5522c8c70e1c0e487f22c2badae0f97d45246223b798ea", size = 1325374, upload-time = "2026-02-14T18:06:12.804Z" }, +] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2026.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/fe/9278c29394bf69169febc21f96b4252c3ee7c8ec22c2fc545004bed47e71/pyinstaller_hooks_contrib-2026.4.tar.gz", hash = "sha256:766c281acb1ecc32e21c8c667056d7ebf5da0aabd5e30c219f9c2a283620eeaa", size = 173050, upload-time = "2026-03-31T14:10:51.188Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/f4/035fb8c06deff827f540a9a4ed9122c54e5376fca3e42eddf0c263730775/pyinstaller_hooks_contrib-2026.4-py3-none-any.whl", hash = "sha256:1de1a5e49a878122010b88c7e295502bc69776c157c4a4dc78741a4e6178b00f", size = 455496, upload-time = "2026-03-31T14:10:49.867Z" }, +] + [[package]] name = "pynput" version = "1.8.1" @@ -578,6 +649,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -755,7 +835,7 @@ wheels = [ [[package]] name = "whisper-local" -version = "0.1.0" +version = "1.0.0" source = { editable = "." } dependencies = [ { name = "darkdetect", marker = "sys_platform == 'win32'" }, @@ -771,6 +851,9 @@ dependencies = [ ] [package.dev-dependencies] +build = [ + { name = "pyinstaller" }, +] dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, @@ -791,6 +874,7 @@ requires-dist = [ ] [package.metadata.requires-dev] +build = [{ name = "pyinstaller", specifier = ">=6.0" }] dev = [ { name = "pytest", specifier = ">=8.0.0" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, diff --git a/whisper_local.spec b/whisper_local.spec new file mode 100644 index 0000000..bf7b8fe --- /dev/null +++ b/whisper_local.spec @@ -0,0 +1,170 @@ +# whisper_local.spec +# PyInstaller-Build-Konfiguration für whisper-local (Windows 64-bit, onedir) +# +# Ausführen: uv run pyinstaller whisper_local.spec --noconfirm +# Oder via: .\build.ps1 + +import sys +from pathlib import Path +from PyInstaller.utils.hooks import collect_data_files + +block_cipher = None + +SP = Path(".venv/Lib/site-packages") + +# ── Binaries ────────────────────────────────────────────────────────────────── +# ctranslate2: Haupt-DLL, CUDA (cudnn) und Intel OpenMP +# DLLs landen in ctranslate2/, damit ctranslate2's os.add_dll_directory() greift +binaries_list = [ + (str(SP / "ctranslate2/ctranslate2.dll"), "ctranslate2"), + (str(SP / "ctranslate2/cudnn64_9.dll"), "ctranslate2"), + (str(SP / "ctranslate2/libiomp5md.dll"), "ctranslate2"), + + # pywin32: COM- und WinTypes-DLLs müssen neben der EXE liegen + (str(SP / "pywin32_system32/pythoncom313.dll"), "."), + (str(SP / "pywin32_system32/pywintypes313.dll"), "."), + + # sounddevice / PortAudio + (str(SP / "_sounddevice_data/portaudio-binaries/libportaudio64bit.dll"), + "_sounddevice_data/portaudio-binaries"), + (str(SP / "_sounddevice_data/portaudio-binaries/libportaudio64bit-asio.dll"), + "_sounddevice_data/portaudio-binaries"), + + # onnxruntime (für Silero VAD in faster-whisper) + (str(SP / "onnxruntime/capi/onnxruntime.dll"), + "onnxruntime/capi"), + (str(SP / "onnxruntime/capi/onnxruntime_providers_shared.dll"), + "onnxruntime/capi"), +] + +# av/FFmpeg: DLL-Namen enthalten Hashes — per Glob gesammelt +av_libs_dir = SP / "av.libs" +binaries_list += [(str(dll), "av.libs") for dll in av_libs_dir.glob("*.dll")] + +# ── Datas ───────────────────────────────────────────────────────────────────── +datas_list = [ + # Silero VAD ONNX-Modell (benötigt von faster-whisper) + (str(SP / "faster_whisper/assets/silero_vad_v6.onnx"), + "faster_whisper/assets"), + + # sv_ttk Theme (TCL-Skripte + PNG-Spritesheets) + (str(SP / "sv_ttk/sv.tcl"), "sv_ttk"), + (str(SP / "sv_ttk/theme"), "sv_ttk/theme"), + + # Beispiel-Konfiguration als Referenz + ("config.example.toml", "."), +] + +# certifi CA-Bundle für HTTPS (huggingface_hub Modell-Download) +datas_list += collect_data_files("certifi") + +# ── Hidden Imports ──────────────────────────────────────────────────────────── +# Alle Module hinter sys.platform-Guards oder lazy imports (importlib, __import__) +hidden_imports_list = [ + # Eigene Windows-Backends (hinter sys.platform == "win32" Guards) + "whisper_local.hotkey._pynput", + "whisper_local.inserter._win32", + "whisper_local.tray._tray", + "whisper_local.tray._icon", + "whisper_local.tray._settings", + "whisper_local.tray._theme", + + # pynput: Backend wird per importlib dynamisch gewählt + "pynput.keyboard._win32", + "pynput.mouse._win32", + "pynput._util", + "pynput._util.win32", + "pynput._util.win32_vks", + + # pywin32 + "win32api", + "win32con", + "win32gui", + "win32clipboard", + "pywintypes", + + # pystray Windows-Backend + Utility-Unterpaket (relative imports) + "pystray._win32", + "pystray._util", + "pystray._util.win32", + + # tkinter: lazy import in tray/_settings.py + "tkinter", + "tkinter.ttk", + + # sv_ttk und darkdetect + "sv_ttk", + "darkdetect", + + # onnxruntime Inference Collection + "onnxruntime.capi.onnxruntime_inference_collection", + + # huggingface_hub für Modell-Download zur Laufzeit + "huggingface_hub", + "huggingface_hub.file_download", + "huggingface_hub.utils", +] + +# ── Excludes ────────────────────────────────────────────────────────────────── +excludes_list = [ + # Linux-only (nicht installiert auf Windows) + "evdev", + "whisper_local.hotkey._evdev", + "whisper_local.inserter._wayland", + # Build- und Test-Infrastruktur + "pytest", + "pytest_asyncio", + "hatchling", + # Nicht benötigt im gebündelten Binary + "unittest", + "doctest", + "pdb", +] + +# ── Analysis ────────────────────────────────────────────────────────────────── +a = Analysis( + ["whisper_local/__main__.py"], + pathex=["."], + binaries=binaries_list, + datas=datas_list, + hiddenimports=hidden_imports_list, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=excludes_list, + 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, # onedir-Modus: DLLs bleiben separat + name="whisper-local", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, # UPX mit CUDA-DLLs deaktiviert + console=False, # kein Konsolenfenster (Tray-App) + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) + +coll = COLLECT( + exe, + a.binaries, + a.zipfiles, + a.datas, + strip=False, + upx=False, + upx_exclude=[], + name="whisper-local", +) diff --git a/whisper_local/transcriber.py b/whisper_local/transcriber.py index 614a13b..55b0c1b 100644 --- a/whisper_local/transcriber.py +++ b/whisper_local/transcriber.py @@ -1,6 +1,8 @@ """Whisper-Transkription via faster-whisper.""" import logging +import sys +from pathlib import Path import numpy as np from faster_whisper import WhisperModel @@ -8,11 +10,24 @@ from faster_whisper import WhisperModel logger = logging.getLogger(__name__) +def _model_cache_dir() -> str | None: + """Im gebündelten Modus: Modell neben der EXE cachen (portable). + Im Entwicklungsmodus: None → HuggingFace-Standard-Cache.""" + if getattr(sys, "frozen", False): + cache = Path(sys.executable).parent / "models" + try: + cache.mkdir(exist_ok=True) + return str(cache) + except OSError: + return None # Fallback auf HuggingFace-Standard-Cache + return None + + class Transcriber: def __init__(self, model_name: str = "small", compute_type: str = "int8", language: str = "de"): self.language = language logger.info("Lade Whisper-Modell '%s' (compute_type=%s)...", model_name, compute_type) - self.model = WhisperModel(model_name, compute_type=compute_type) + self.model = WhisperModel(model_name, compute_type=compute_type, download_root=_model_cache_dir()) logger.info("Modell geladen") def transcribe(self, audio: np.ndarray) -> str: