diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 46e121c..21b9c57 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -29,7 +29,14 @@ "Bash(.venv-windows/Scripts/python.exe -c \"\nimport os, site\nsp = site.getsitepackages\\(\\)[0]\ncublas = os.path.join\\(sp, 'nvidia', 'cublas', 'bin'\\)\ncudnn = os.path.join\\(sp, 'nvidia', 'cudnn', 'bin'\\)\nprint\\('cublas:', os.path.exists\\(cublas\\), cublas\\)\nprint\\('cudnn:', os.path.exists\\(cudnn\\), cudnn\\)\n\")", "Bash(.venv-windows/Scripts/python.exe -c \"\nimport os\nsp = '.venv-windows/Lib/site-packages'\ncublas = os.path.join\\(sp, 'nvidia', 'cublas', 'bin'\\)\ncudnn = os.path.join\\(sp, 'nvidia', 'cudnn', 'bin'\\)\nprint\\('cublas:', os.path.exists\\(cublas\\), os.path.abspath\\(cublas\\)\\)\nprint\\('cudnn:', os.path.exists\\(cudnn\\), os.path.abspath\\(cudnn\\)\\)\n\")", "Bash(.venv-windows/Scripts/python.exe -c \"import site; print\\(site.getsitepackages\\(\\)\\)\")", - "Bash(git push:*)" + "Bash(git push:*)", + "Bash(find /run/media/chk/Ventoy/projects/chrka/whisper-dictation -type f \\\\\\(-name .github -o -name *.yml -o -name *.yaml \\\\\\))", + "Bash(chmod +x /run/media/chk/Ventoy/projects/chrka/whisper-dictation/build-linux.sh)", + "Bash(bash install.sh)", + "Bash(bash build-linux.sh)", + "Bash(.venv-linux/bin/python -c \"import tkinter; print\\(''tkinter OK''\\)\")", + "Bash(pacman -Q tk)", + "Bash(sudo pacman:*)" ] } } diff --git a/build-linux.sh b/build-linux.sh new file mode 100644 index 0000000..5162a25 --- /dev/null +++ b/build-linux.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")" +.venv-linux/bin/python build.py diff --git a/build.py b/build.py index 0051dac..1a6955c 100644 --- a/build.py +++ b/build.py @@ -3,29 +3,40 @@ import os import shutil import sys import subprocess -from PIL import Image +from PIL import Image, ImageDraw + +_IS_WINDOWS = sys.platform == "win32" +_PLATFORM_TAG = "windows" if _IS_WINDOWS else "linux" + + +def _make_icon_image(size: int) -> Image.Image: + """Create a green-dot icon at the given size.""" + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + d = ImageDraw.Draw(img) + margin = max(1, size // 16) + d.ellipse([margin, margin, size - margin, size - margin], fill=(40, 200, 80)) + return img + def generate_icon(): - """Generate icon.ico from tray icon colors (green dot on transparent).""" + """Generate platform-appropriate icon files.""" sizes = [16, 32, 48, 256] - frames = [] - for size in sizes: - img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) - from PIL import ImageDraw - d = ImageDraw.Draw(img) - margin = max(1, size // 16) - d.ellipse([margin, margin, size - margin, size - margin], fill=(40, 200, 80)) - frames.append(img) - frames[0].save("icon.ico", format="ICO", sizes=[(s, s) for s in sizes], - append_images=frames[1:]) - print("icon.ico generated.") + frames = [_make_icon_image(s) for s in sizes] + if _IS_WINDOWS: + frames[0].save("icon.ico", format="ICO", sizes=[(s, s) for s in sizes], + append_images=frames[1:]) + print("icon.ico generated.") + else: + frames[-1].save("icon.png", format="PNG") + print("icon.png generated.") + def build(): generate_icon() subprocess.run([sys.executable, "-m", "PyInstaller", "whisper-dictation.spec", "--noconfirm"], check=True) - dist_dir = os.path.join("dist", "whisper-dictation") + dist_dir = os.path.join("dist", "whisper-dictation-" + _PLATFORM_TAG) for fname in ["config.json", "vocabulary.json"]: dest = os.path.join(dist_dir, fname) if not os.path.exists(dest): @@ -34,7 +45,15 @@ def build(): else: print(f"Skipped {fname} (already exists in dist — preserving user edits)") + # Copy icon.png into dist for Linux .desktop files + if not _IS_WINDOWS: + icon_dest = os.path.join(dist_dir, "icon.png") + if not os.path.exists(icon_dest): + shutil.copy("icon.png", icon_dest) + print(f"Copied icon.png -> {dist_dir}/") + print(f"\nBuild complete: {dist_dir}/") + if __name__ == "__main__": build() diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..506168b Binary files /dev/null and b/icon.png differ diff --git a/install.sh b/install.sh index a6ef1a7..abd7cc8 100644 --- a/install.sh +++ b/install.sh @@ -1,13 +1,15 @@ -#!/bin/bash -set -e -cd "$(dirname "$0")" - -echo "Creating Linux venv (.venv-linux)..." -python3 -m venv --system-site-packages .venv-linux - -echo "Installing dependencies..." -.venv-linux/bin/pip install --upgrade pip -.venv-linux/bin/pip install -r requirements.txt -# No CUDA deps on Linux — runs on CPU - -echo "Done. Run ./start.sh to launch." +#!/bin/bash +set -e +cd "$(dirname "$0")" + +echo "Creating Linux venv (.venv-linux)..." +python3 -m venv --system-site-packages .venv-linux + +echo "Installing dependencies..." +.venv-linux/bin/pip install --upgrade pip +.venv-linux/bin/pip install -r requirements.txt +.venv-linux/bin/pip install pyinstaller +# CUDA: uses system-installed CUDA libs (no pip CUDA packages needed) + +echo "" +echo "Done. Run ./start.sh to launch, or ./build-linux.sh to create a standalone build." diff --git a/start.sh b/start.sh index 1acc278..88393e3 100644 --- a/start.sh +++ b/start.sh @@ -1,3 +1,3 @@ -#!/bin/bash -cd "$(dirname "$0")" -.venv-linux/bin/python -u main.py +#!/bin/bash +cd "$(dirname "$0")" +.venv-linux/bin/python -u main.py diff --git a/whisper-dictation.spec b/whisper-dictation.spec index 186455b..a5938c5 100644 --- a/whisper-dictation.spec +++ b/whisper-dictation.spec @@ -11,27 +11,38 @@ def _pkg_path(pkg): sp = site.getsitepackages()[0] return os.path.join(sp, *pkg.split('.')) -_sp = next(p for p in site.getsitepackages() if p.endswith('site-packages')) -_nvidia = os.path.join(_sp, 'nvidia') +_is_windows = sys.platform == 'win32' +_platform_tag = 'windows' if _is_windows else 'linux' + +# ── Platform-specific binaries ──────────────────────────────────────────────── +_binaries = [] +if _is_windows: + _sp = next(p for p in site.getsitepackages() if p.endswith('site-packages')) + _nvidia = os.path.join(_sp, 'nvidia') + _binaries = [ + (os.path.join(_nvidia, 'cublas', 'bin', '*.dll'), '.'), + (os.path.join(_nvidia, 'cudnn', 'bin', '*.dll'), '.'), + ] + +# ── Platform-specific hidden imports ────────────────────────────────────────── +_hiddenimports = [ + 'ctranslate2', + 'faster_whisper', + 'sounddevice', +] +if _is_windows: + _hiddenimports.append('pynput.keyboard._win32') +else: + _hiddenimports += ['pynput.keyboard._xorg', 'pynput.keyboard._uinput'] a = Analysis( ['main.py'], pathex=[], - binaries=[ - (os.path.join(_nvidia, 'cublas', 'bin', '*.dll'), '.'), - (os.path.join(_nvidia, 'cudnn', 'bin', '*.dll'), '.'), - ], + binaries=_binaries, datas=[ (os.path.join(_pkg_path('faster_whisper'), 'assets', '*.onnx'), 'faster_whisper/assets'), ], - hiddenimports=[ - 'ctranslate2', - 'faster_whisper', - 'sounddevice', - 'pynput.keyboard._win32', - 'pynput.keyboard._xorg', - 'pynput.keyboard._uinput', - ], + hiddenimports=_hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[], @@ -49,8 +60,8 @@ exe = EXE( bootloader_ignore_signals=False, strip=False, upx=True, - console=False, - icon='icon.ico', + console=not _is_windows, + icon='icon.ico' if _is_windows else None, ) coll = COLLECT( exe, @@ -59,5 +70,5 @@ coll = COLLECT( strip=False, upx=True, upx_exclude=[], - name='whisper-dictation', + name='whisper-dictation-' + _platform_tag, )