diff --git a/.gitignore b/.gitignore
index 7ebcc17..f7763f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,7 +7,8 @@ __pycache__/
*.egg
dist/
build/
-*.spec
+# *.spec -> We want to track VoxFlow.spec
+!VoxFlow.spec
# Virtual environments
venv/
diff --git a/BUILD_EXE.bat b/BUILD_EXE.bat
index 9d49c64..c0cb4e4 100644
--- a/BUILD_EXE.bat
+++ b/BUILD_EXE.bat
@@ -1,91 +1,109 @@
@echo off
-title VoxFlow - Building .exe + Installer (AI Evolution Polska)
+chcp 65001 >nul
+cd /d "%~dp0"
+title VoxFlow — Build Installer
+
echo.
-echo ╔═══════════════════════════════════════════════════╗
-echo ║ 🔨 Building VoxFlow .exe + Installer ║
-echo ║ by AI Evolution Polska ║
-echo ╚═══════════════════════════════════════════════════╝
+echo ╔══════════════════════════════════════════════════════════╗
+echo ║ VoxFlow — Build Setup.exe Installer ║
+echo ║ by AI Evolution Polska ║
+echo ╚══════════════════════════════════════════════════════════╝
echo.
-REM Activate virtual environment
+REM ─── Step 0: Check venv ─────────────────────────────────────────
+if not exist "venv\Scripts\activate.bat" (
+ echo ❌ Środowisko venv nie istnieje. Uruchom najpierw install.bat
+ pause & exit /b 1
+)
call venv\Scripts\activate.bat
-REM Install PyInstaller if needed
-pip install pyinstaller --quiet --disable-pip-version-check 2>nul
+REM ─── Step 1: Install/update build tools ─────────────────────────
+echo [1/4] Instalowanie narzędzi build...
+pip install pyinstaller --quiet --disable-pip-version-check
+if %errorlevel% neq 0 (
+ echo ❌ Błąd instalacji PyInstaller!
+ pause & exit /b 1
+)
+echo ✅ PyInstaller gotowy
+
+REM ─── Step 2: Clean previous build ───────────────────────────────
+echo.
+echo [2/4] Czyszczenie poprzedniego buildu...
+if exist "dist\VoxFlow" rmdir /s /q "dist\VoxFlow"
+if exist "build" rmdir /s /q "build"
+echo ✅ Wyczyszczono
+REM ─── Step 3: Build EXE with PyInstaller ─────────────────────────
echo.
-echo ═══════════════════════════════════════════
-echo STEP 1: Building VoxFlow .exe
-echo ═══════════════════════════════════════════
+echo [3/4] Budowanie VoxFlow.exe przez PyInstaller...
+echo (To może potrwać 2-5 minut — proszę czekać)
echo.
-pyinstaller ^
- --name "VoxFlow" ^
- --icon "assets\voxflow.ico" ^
- --onedir ^
- --windowed ^
- --noconfirm ^
- --clean ^
- --add-data "voxflow;voxflow" ^
- --add-data "assets;assets" ^
- --hidden-import "faster_whisper" ^
- --hidden-import "ctranslate2" ^
- --hidden-import "huggingface_hub" ^
- --hidden-import "tokenizers" ^
- --hidden-import "sounddevice" ^
- --hidden-import "customtkinter" ^
- --hidden-import "pystray" ^
- --hidden-import "keyboard" ^
- --hidden-import "pyperclip" ^
- --hidden-import "_sounddevice_data" ^
- --hidden-import "scipy" ^
- --collect-all "customtkinter" ^
- --collect-all "faster_whisper" ^
- --collect-all "ctranslate2" ^
- voxflow/main.py
+pyinstaller VoxFlow.spec --noconfirm --clean
if %errorlevel% neq 0 (
echo.
- echo ❌ PyInstaller build failed!
- deactivate
- pause
- exit /b 1
+ echo ❌ PyInstaller zakończył się błędem!
+ echo Sprawdź logi powyżej.
+ pause & exit /b 1
+)
+if not exist "dist\VoxFlow\VoxFlow.exe" (
+ echo ❌ Nie znaleziono dist\VoxFlow\VoxFlow.exe!
+ pause & exit /b 1
)
echo.
-echo ✅ .exe built successfully at: dist\VoxFlow\VoxFlow.exe
-echo.
+echo ✅ VoxFlow.exe zbudowany pomyślnie
-echo ═══════════════════════════════════════════
-echo STEP 2: Creating Installer (optional)
-echo ═══════════════════════════════════════════
+REM ─── Step 4: Build Installer with Inno Setup ────────────────────
+echo.
+echo [4/4] Tworzenie instalatora VoxFlow_Setup.exe...
echo.
-REM Check if Inno Setup is installed
+REM Possible Inno Setup locations
+set ISCC=""
if exist "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" (
- echo 📦 Inno Setup found! Building installer...
- "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer.iss
- if %errorlevel% equ 0 (
- echo.
- echo ✅ Installer created at: installer_output\VoxFlow_Setup_v1.0.0.exe
- ) else (
- echo ⚠️ Installer build failed. You can still use the portable .exe.
- )
-) else (
- echo ℹ️ Inno Setup not found. Skipping installer creation.
- echo Install from: https://jrsoftware.org/isinfo.php
- echo Then re-run this script to create an installer.
+ set ISCC="C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
+)
+if exist "C:\Program Files\Inno Setup 6\ISCC.exe" (
+ set ISCC="C:\Program Files\Inno Setup 6\ISCC.exe"
+)
+
+if %ISCC%=="" (
+ echo ⚠️ Inno Setup nie jest zainstalowany!
+ echo.
+ echo Pobierz Inno Setup 6 ze strony:
+ echo https://jrsoftware.org/isinfo.php
+ echo.
+ echo Następnie uruchom ten skrypt ponownie, aby stworzyć
+ echo plik instalacyjny VoxFlow_Setup.exe
+ echo.
+ echo Gotowy EXE znajduje się w: dist\VoxFlow\VoxFlow.exe
+ pause
+ start "" explorer "dist\VoxFlow"
+ exit /b 0
+)
+
+if not exist "installer_output" mkdir installer_output
+
+%ISCC% installer.iss /Q
+
+if %errorlevel% neq 0 (
echo.
- echo 💡 You can still use the portable .exe at: dist\VoxFlow\VoxFlow.exe
+ echo ❌ Inno Setup zakończył się błędem!
+ echo Sprawdź plik installer.iss
+ pause & exit /b 1
)
echo.
-echo ══════════════════════════════════════
-echo 📂 Output files:
-echo dist\VoxFlow\VoxFlow.exe (portable)
-if exist "installer_output\VoxFlow_Setup_v1.0.0.exe" echo installer_output\VoxFlow_Setup_v1.0.0.exe (installer)
-echo ══════════════════════════════════════
+echo ╔══════════════════════════════════════════════════════════╗
+echo ║ ║
+echo ║ ✅ GOTOWE! VoxFlow_Setup.exe zbudowany pomyślnie ║
+echo ║ ║
+echo ║ Lokalizacja: installer_output\VoxFlow_Setup_v*.exe ║
+echo ║ ║
+echo ╚══════════════════════════════════════════════════════════╝
echo.
-deactivate
+start "" explorer "installer_output"
pause
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0df4aae..fb7434b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,35 @@ Wszystkie istotne zmiany w projekcie są dokumentowane w tym pliku.
Format oparty na [Keep a Changelog](https://keepachangelog.com/pl/1.0.0/),
wersjonowanie zgodne z [Semantic Versioning](https://semver.org/lang/pl/).
+---
+
+## [1.1.0] — 2026-02-20
+
+### ✨ Dodane
+- 🎤 Wybór mikrofonu — dropdown z listą urządzeń audio
+- ⌨️ Interaktywny hotkey picker — naciśnij dowolny klawisz aby ustawić
+- 🔑 Wsparcie kombinacji klawiszy (Ctrl+Space, Ctrl+Shift+Space itp.)
+- 🖥️ Zapamiętywanie rozmiaru okna między sesjami
+- ⚠️ Automatyczny fallback mikrofonu gdy wybrany jest niedostępny
+
+### 🐛 Naprawiono
+- **[Krytyczny]** Hotkey resetował się do F2 po każdym restarcie — walidacja
+ konfiguracji blokowała własne klawisze użytkownika
+- **[Krytyczny]** Fallback mikrofonu nie aktualizował konfiguracji trwale —
+ problem powtarzał się przy każdym uruchomieniu
+- **[Krytyczny]** `keyboard.read_event()` bez timeout mogło zawiesić wątek UI
+ podczas hotkey capture — naprawiono przez threading.Event z limitem czasu
+- **[Ważny]** Settings panel wstawiał się w losowe miejsce — naprawiono przez
+ przechowywanie referencji do `footer_frame`
+- Graceful degradation gdy `pystray` lub `autostart` są niedostępne (brak crashu)
+
+### 🔧 Poprawki jakości
+- Escape anuluje teraz hotkey capture
+- Debounced zapis rozmiaru okna (400ms po ostatnim zdarzeniu resize)
+- Tray guard — aplikacja działa poprawnie bez system tray
+
+---
+
## [1.0.0] — 2026-02-16
### ✨ Dodane
@@ -38,4 +67,4 @@ wersjonowanie zgodne z [Semantic Versioning](https://semver.org/lang/pl/).
---
-*Zbudowane przez [AI Evolution Polska](https://github.com/AI-Evolution-Polska)*
+*Zbudowane przez [AI Evolution Polska](https://github.com/aievolutionpl)*
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
index 82408c2..0d77f22 100644
--- a/CODE_OF_CONDUCT.md
+++ b/CODE_OF_CONDUCT.md
@@ -28,4 +28,4 @@ Ten kodeks oparty jest na [Contributor Covenant v2.1](https://www.contributor-co
---
-*Projekt [AI Evolution Polska](https://github.com/AI-Evolution-Polska)*
+*Projekt [AI Evolution Polska](https://github.com/aievolutionpl)*
diff --git a/CREATE_PORTABLE.bat b/CREATE_PORTABLE.bat
new file mode 100644
index 0000000..a39e4ac
--- /dev/null
+++ b/CREATE_PORTABLE.bat
@@ -0,0 +1,133 @@
+@echo off
+chcp 65001 >nul
+cd /d "%~dp0"
+title VoxFlow — Tworzenie wersji Portable
+
+echo.
+echo ╔══════════════════════════════════════════════════════════╗
+echo ║ VoxFlow — Tworzenie wersji Portable ║
+echo ║ by AI Evolution Polska ║
+echo ╚══════════════════════════════════════════════════════════╝
+echo.
+
+REM ─── Ustal wersję ──────────────────────────────────────────────
+set VER=1.1.0
+set PORTABLE_DIR=dist\VoxFlow_Portable_v%VER%
+set PORTABLE_APP=%PORTABLE_DIR%\VoxFlow
+
+REM ─── Krok 0: Sprawdź venv ──────────────────────────────────────
+if not exist "venv\Scripts\activate.bat" (
+ echo ❌ Nie znaleziono środowiska venv.
+ echo Uruchom najpierw install.bat
+ echo.
+ pause & exit /b 1
+)
+call venv\Scripts\activate.bat
+
+REM ─── Krok 1: Zbuduj EXE jeśli nie istnieje ────────────────────
+echo [1/4] Sprawdzam czy VoxFlow.exe jest zbudowany...
+if exist "dist\VoxFlow\VoxFlow.exe" (
+ echo ✅ VoxFlow.exe już istnieje — pomijam build
+) else (
+ echo ⏳ Buduję VoxFlow.exe przez PyInstaller...
+ echo (To może potrwać 3-6 minut)
+ pip install pyinstaller --quiet --disable-pip-version-check
+ pyinstaller VoxFlow.spec --noconfirm --clean
+ if %errorlevel% neq 0 (
+ echo ❌ Build nie powiódł się!
+ pause & exit /b 1
+ )
+ if not exist "dist\VoxFlow\VoxFlow.exe" (
+ echo ❌ Nie znaleziono dist\VoxFlow\VoxFlow.exe
+ pause & exit /b 1
+ )
+ echo ✅ VoxFlow.exe zbudowany
+)
+
+REM ─── Krok 2: Wyczyść stary portable folder ────────────────────
+echo.
+echo [2/4] Przygotowanie katalogu portable...
+if exist "%PORTABLE_DIR%" (
+ echo 🗑 Usuwam stary folder %PORTABLE_DIR%...
+ rmdir /s /q "%PORTABLE_DIR%"
+)
+mkdir "%PORTABLE_DIR%"
+
+REM ─── Krok 3: Kopiuj pliki ─────────────────────────────────────
+echo.
+echo [3/4] Kopiowanie plików VoxFlow...
+xcopy "dist\VoxFlow\*" "%PORTABLE_APP%\" /E /I /Q
+if %errorlevel% neq 0 (
+ echo ❌ Błąd kopiowania plików!
+ pause & exit /b 1
+)
+
+REM ─── Stwórz launcher START_VOXFLOW.bat ────────────────────────
+echo @echo off > "%PORTABLE_DIR%\START_VOXFLOW.bat"
+echo chcp 65001 ^>nul >> "%PORTABLE_DIR%\START_VOXFLOW.bat"
+echo cd /d "%%~dp0VoxFlow" >> "%PORTABLE_DIR%\START_VOXFLOW.bat"
+echo start "" "VoxFlow.exe" >> "%PORTABLE_DIR%\START_VOXFLOW.bat"
+
+REM ─── Stwórz README_PORTABLE.txt ───────────────────────────────
+(
+echo VoxFlow %VER% — Wersja Portable
+echo ================================
+echo.
+echo JAK URUCHOMIĆ:
+echo 1. Kliknij dwukrotnie START_VOXFLOW.bat
+echo (lub uruchom VoxFlow\VoxFlow.exe bezpośrednio)
+echo.
+echo JAK DZIAŁA:
+echo - Przytrzymaj F2 i mów - pojawi sie animacja nagrywania na dole ekranu
+echo - Zwolnij F2 - tekst zostanie wklejony w aktywnym oknie
+echo - Kliknij przycisk klawisza w UI zeby zmienić skrót klawiszowy
+echo - Ikona w zasobniku - kliknij prawym przyciskiem dla opcji
+echo.
+echo WYMAGANIA:
+echo - Windows 10/11 (64-bit)
+echo - Mikrofon
+echo - Połączenie z internetem (TYLKO przy pierwszym uruchomieniu
+echo - do pobrania modelu AI ~500 MB)
+echo.
+echo WAŻNE:
+echo - NIE przenoś samego VoxFlow.exe bez całego folderu VoxFlow\
+echo - Możesz przenieść cały folder VoxFlow_Portable_v%VER% gdzie chcesz
+echo - Ustawienia są zapisywane w: %%APPDATA%%\VoxFlow\
+echo.
+echo by AI Evolution Polska - https://github.com/aievolutionpl/VoxFlow
+) > "%PORTABLE_DIR%\README_PORTABLE.txt"
+
+echo.
+echo [4/4] Gotowe!
+echo.
+
+REM ─── Zapytaj czy zipować ──────────────────────────────────────
+set /p DOZIP="Czy spakować do ZIP? [T/n]: "
+if /i "%DOZIP%"=="n" goto :done
+
+echo.
+echo 📦 Pakuję do ZIP...
+
+REM Użyj PowerShell do zipowania
+powershell -Command "Compress-Archive -Path '%PORTABLE_DIR%' -DestinationPath 'dist\VoxFlow_Portable_v%VER%.zip' -Force"
+
+if %errorlevel% equ 0 (
+ echo ✅ ZIP stworzony: dist\VoxFlow_Portable_v%VER%.zip
+) else (
+ echo ⚠️ Nie udało się stworzyć ZIP. Folder istnieje: %PORTABLE_DIR%
+)
+
+:done
+echo.
+echo ╔══════════════════════════════════════════════════════════╗
+echo ║ ║
+echo ║ ✅ Wersja Portable gotowa! ║
+echo ║ ║
+echo ║ Folder: %PORTABLE_DIR% ║
+echo ║ Launcher: START_VOXFLOW.bat ║
+echo ║ ║
+echo ╚══════════════════════════════════════════════════════════╝
+echo.
+
+start "" explorer "%PORTABLE_DIR%"
+pause
diff --git a/INSTALL_MAC.md b/INSTALL_MAC.md
new file mode 100644
index 0000000..76dc956
--- /dev/null
+++ b/INSTALL_MAC.md
@@ -0,0 +1,91 @@
+# 🎤 VoxFlow — Instalacja na macOS
+
+**VoxFlow** — bezpłatne, lokalne narzędzie do dyktowania głosem. 100% offline.
+by [AI Evolution Polska](https://github.com/aievolutionpl)
+
+---
+
+## ⚡ Instalacja jednym kliknięciem
+
+### Krok 1 — Pobierz VoxFlow
+
+```bash
+git clone https://github.com/aievolutionpl/VoxFlow.git
+cd VoxFlow
+```
+
+---
+
+### Krok 2 — Uruchom instalator
+
+```bash
+chmod +x install_mac.sh
+./install_mac.sh
+```
+
+Skrypt automatycznie:
+- ✅ Sprawdzi i zainstaluje [Homebrew](https://brew.sh) (jeśli brakuje)
+- ✅ Zainstaluje `portaudio` (wymagane przez sounddevice)
+- ✅ Sprawdzi Python 3.9+ (zainstaluje jeśli brakuje)
+- ✅ Stworzy wirtualne środowisko (`venv`)
+- ✅ Zainstaluje wszystkie biblioteki
+- ✅ Stworzy launcher `VoxFlow.command`
+
+---
+
+### Krok 3 — Uruchomienie
+
+Kliknij dwukrotnie plik `VoxFlow.command` lub uruchom:
+
+```bash
+./VoxFlow.command
+```
+
+---
+
+## ⚠️ Uprawnienia Accessibility (globalny hotkey)
+
+> [!IMPORTANT]
+> Aby skrót klawiszowy (np. F2) działał we **wszystkich** aplikacjach, macOS wymaga przyznania uprawnień Accessibility.
+
+1. Otwórz **System Settings** → **Privacy & Security** → **Accessibility**
+2. Kliknij `+` i dodaj:
+ - Terminal (jeśli uruchamiasz przez terminal)
+ - lub `VoxFlow.command`
+3. Uruchom VoxFlow ponownie
+
+> [!NOTE]
+> Bez tych uprawnień hotkey działa tylko gdy VoxFlow jest aktywną aplikacją na pierwszym planie.
+
+---
+
+## 🍎 Apple Silicon (M1/M2/M3)
+
+Skrypt automatycznie wykrywa architekturę. Na Apple Silicon:
+- Homebrew instaluje się do `/opt/homebrew/`
+- `faster-whisper` obsługuje ARM64 natywnie
+- Wydajność jest doskonała bez GPU
+
+---
+
+## ❓ Częste problemy
+
+| Problem | Rozwiązanie |
+|---------|-------------|
+| `command not found: brew` | Zainstaluj [Homebrew](https://brew.sh) ręcznie |
+| `portaudio` błąd | `brew install portaudio` |
+| Python za stary | `brew install python@3.11` |
+| Hotkey nie działa globalnie | Dodaj do Accessibility (powyżej) |
+| Błąd pobierania modelu | Sprawdź połączenie internetowe |
+
+---
+
+## 📋 Wymagania systemowe
+
+- macOS 11 Big Sur lub nowszy
+- Intel lub Apple Silicon (M1/M2/M3)
+- Python 3.9–3.12
+- Homebrew (instalowany automatycznie)
+- 4 GB RAM (8 GB zalecane)
+- Mikrofon
+- Połączenie internetowe (pierwsze pobranie modelu ~500 MB)
diff --git a/INSTALL_WINDOWS.md b/INSTALL_WINDOWS.md
new file mode 100644
index 0000000..7bfd119
--- /dev/null
+++ b/INSTALL_WINDOWS.md
@@ -0,0 +1,98 @@
+# 🎤 VoxFlow — Instalacja na Windows
+
+**VoxFlow** — bezpłatne, lokalne narzędzie do dyktowania głosem. 100% offline. Brak chmury.
+by [AI Evolution Polska](https://github.com/aievolutionpl)
+
+---
+
+## ⚡ Instalacja jednym kliknięciem
+
+### Krok 1 — Pobierz Python 3.10+
+
+> [!IMPORTANT]
+> Python **musi** być zainstalowany przed uruchomieniem instalatora VoxFlow.
+> **Zaznacz opcję "Add Python to PATH"** podczas instalacji!
+
+👉 [Pobierz Python ze python.org](https://www.python.org/downloads/)
+
+Sprawdź instalację (otwórz `cmd`):
+```
+python --version
+```
+Powinno wyświetlić: `Python 3.10.x` lub nowszy.
+
+---
+
+### Krok 2 — Pobierz VoxFlow
+
+Pobierz i rozpakuj archiwum lub sklonuj repozytorium:
+
+```bash
+git clone https://github.com/aievolutionpl/VoxFlow.git
+cd VoxFlow
+```
+
+---
+
+### Krok 3 — Uruchom instalator
+
+Kliknij dwukrotnie plik:
+
+```
+install.bat
+```
+
+Skrypt automatycznie:
+- ✅ Sprawdzi wersję Python
+- ✅ Stworzy wirtualne środowisko (`venv`)
+- ✅ Zainstaluje wszystkie biblioteki
+- ✅ Zweryfikuje instalację
+- ✅ Zaproponuje natychmiastowe uruchomienie
+
+---
+
+### Krok 4 — Uruchomienie
+
+Po instalacji kliknij:
+```
+START_VOXFLOW.bat
+```
+
+Lub uruchom ręcznie:
+```bash
+venv\Scripts\activate
+python -m voxflow.main
+```
+
+---
+
+## 🔨 Budowanie instalatora .exe (opcjonalne)
+
+Jeśli chcesz stworzyć samodzielny plik `.exe` do dystrybucji:
+
+```bash
+BUILD_EXE.bat
+```
+
+Wymagany [Inno Setup 6](https://jrsoftware.org/isinfo.php) do stworzenia instalatora `.exe`.
+
+---
+
+## ❓ Częste problemy
+
+| Problem | Rozwiązanie |
+|---------|-------------|
+| `python` nie znany | Zainstaluj Python i dodaj do PATH |
+| Błąd importu sounddevice | `pip install sounddevice` z C++ Redistributable |
+| Hotkey nie działa | Uruchom jako Administrator |
+| Błąd modelu Whisper | Sprawdź połączenie internetowe (tylko pierwsze pobieranie) |
+
+---
+
+## 📋 Wymagania systemowe
+
+- Windows 10 / 11 (64-bit)
+- Python 3.9–3.12
+- 4 GB RAM (8 GB zalecane dla modelu `medium`)
+- Mikrofon
+- Połączenie internetowe (tylko do pierwszego pobrania modelu ~500 MB)
diff --git a/README.md b/README.md
index a366141..4fa44d0 100644
--- a/README.md
+++ b/README.md
@@ -1,244 +1,134 @@
-
-
-
-
-🎤 VoxFlow
-
-
- Lokalne rozpoznawanie mowy — 100% offline, 100% prywatne
- Local Speech-to-Text for Windows • Polish & English
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
----
+# 🎤 VoxFlow
-## ✨ Co to jest VoxFlow?
+### Dyktowanie głosem • 100% Offline • Open Source
-**VoxFlow** to darmowa, w pełni lokalna aplikacja do zamiany mowy na tekst dla systemu Windows. Przytrzymaj klawisz **F2**, mów — a VoxFlow automatycznie wpisze tekst w aktywne okno. Żadne dane nie opuszczają Twojego komputera.
+[](LICENSE)
+[](https://python.org)
+[](https://github.com/aievolutionpl/VoxFlow)
+[](https://github.com/aievolutionpl)
-### 🎯 Kluczowe funkcje
+VoxFlow to bezpłatne narzędzie do dyktowania głosem.
+Przytrzymaj klawisz → mów → tekst pojawia się w aktywnym oknie. Zero chmury, zero subskrypcji.
-| Funkcja | Opis |
-|---------|------|
-| 🎤 **Hold-to-Record** | Przytrzymaj F2 (lub inny klawisz) i mów |
-| 🇵🇱🇬🇧 **Polski i Angielski** | Automatyczne wykrywanie języka |
-| 🔒 **100% Lokalne** | Zero internetu, zero chmury, zero śledzenia |
-| ✍️ **Auto-wpisywanie** | Tekst trafia od razu do aktywnego okna |
-| 📋 **Auto-kopiowanie** | Transkrypcja ląduje w schowku |
-| 🧠 **5 modeli AI** | Od tiny (75 MB) po large-v3 (3 GB) |
-| ✨ **Autokorekta** | Poprawia polskie znaki diakrytyczne |
-| 🎯 **VAD** | Inteligentna detekcja mowy — ignoruje ciszę |
-| 🔊 **Dźwięki** | Sygnał start/stop nagrywania |
-| 🖥️ **System Tray** | Minimalizuj do zasobnika systemowego |
-| 🔴 **Overlay REC** | Animowana nakładka "nagrywam" na ekranie |
-| 📚 **Historia** | Ostatnie 10 transkrypcji z kopiowaniem |
-| 🎨 **Ciemny motyw** | Piękny UI z animacjami |
+
---
-## 🚀 Instalacja
-
-### Sposób 1: Pobierz gotowy .exe (zalecane)
-
-1. Przejdź do [Releases](../../releases)
-2. Pobierz `VoxFlow_Setup_v1.0.0.exe` (instalator) lub `VoxFlow-Portable.zip` (wersja portable)
-3. Uruchom i gotowe!
-
-### Sposób 2: Uruchomienie jednym kliknięciem (ze źródeł)
+## ⚡ Instalacja Windows — krok po kroku
-```
-Kliknij dwukrotnie: START_VOXFLOW.bat
-```
+> Wymagania: Windows 10/11 (64-bit), mikrofon, internet (tylko przy pierwszym uruchomieniu — pobranie modelu AI ~500 MB)
-Skrypt automatycznie:
-- ✅ Sprawdzi Python
-- ✅ Stworzy środowisko wirtualne
-- ✅ Zainstaluje pakiety
-- ✅ Uruchomi VoxFlow
+### Krok 1 — Zainstaluj Python
-### Sposób 3: Ręczne uruchomienie
-
-```bash
-# Sklonuj repozytorium
-git clone https://github.com/AI-Evolution-Polska/VoxFlow.git
-cd VoxFlow
+1. Wejdź na stronę [python.org/downloads](https://www.python.org/downloads/)
+2. Pobierz Python 3.10 lub nowszy
+3. Uruchom instalator
+4. WAŻNE: zaznacz opcję "Add Python to PATH" przed kliknięciem Install
-# Stwórz środowisko wirtualne
-python -m venv venv
-venv\Scripts\activate
+### Krok 2 — Zainstaluj VoxFlow
-# Zainstaluj zależności
-pip install -r requirements.txt
+1. Pobierz lub rozpakuj folder VoxFlow na dysk
+2. Kliknij dwukrotnie plik `install.bat`
+3. Poczekaj aż zakończy instalację (2–5 minut) — instaluje wymagane biblioteki AI
-# Uruchom
-python -m voxflow.main
-```
+### Krok 3 — Uruchom
-### Sposób 4: pip install
+Kliknij dwukrotnie `START_VOXFLOW.bat` (tworzony automatycznie przez install.bat).
-```bash
-pip install .
-voxflow
-```
+Gotowe — VoxFlow jest teraz uruchomiony i widoczny w zasobniku systemowym (prawy dolny róg ekranu).
---
-## ⌨️ Jak używać
-
-1. **Uruchom VoxFlow** — okno aplikacji pojawi się na ekranie
-2. **Przytrzymaj F2** (lub kliknij przycisk mikrofonu)
-3. **Mów** — zobaczysz animację nagrywania
-4. **Puść F2** — tekst zostanie automatycznie:
- - 📝 Wyświetlony w oknie VoxFlow
- - ✍️ Wpisany do aktywnego okna (jeśli włączone)
- - 📋 Skopiowany do schowka (jeśli włączone)
+## 🖥️ Jak używać
-> 💡 **Tip**: VoxFlow działa w tle — możesz go zminimalizować do zasobnika systemowego.
+| Akcja | Efekt |
+|-------|-------|
+| Przytrzymaj `F2` i mów | Nagrywa — na dole ekranu pojawia się animacja fal |
+| Zwolnij `F2` | AI przetwarza mowę i wkleja tekst w aktywnym oknie |
+| Kliknij przycisk klawisza w UI | Zmień skrót klawiszowy na dowolny inny |
+| Kliknij dropdown mikrofonu | Wybierz urządzenie audio |
+| Kliknij ikonę w zasobniku prawym przyciskiem | Menu: pokaż okno, nagraj, zamknij |
---
-## 🧠 Modele Whisper
-
-Przy pierwszym użyciu VoxFlow automatycznie pobierze wybrany model (~1 min dla `small`).
+## 📦 Wersja EXE (bez Pythona)
-| Model | Rozmiar | Jakość 🇵🇱 | Szybkość | Rekomendacja |
-|-------|---------|-----------|----------|--------------|
-| `tiny` | ~75 MB | ⭐⭐ | ⚡⚡⚡⚡⚡ | Szybkie testy |
-| `base` | ~150 MB | ⭐⭐⭐ | ⚡⚡⚡⚡ | Podstawowe użycie |
-| `small` | ~500 MB | ⭐⭐⭐⭐ | ⚡⚡⚡ | **Zalecany** ⬅️ |
-| `medium` | ~1.5 GB | ⭐⭐⭐⭐⭐ | ⚡⚡ | Wysoka jakość |
-| `large-v3` | ~3 GB | ⭐⭐⭐⭐⭐ | ⚡ | Maksymalna dokładność |
+Jeśli chcesz wersję, która nie wymaga instalacji Pythona:
-> 🎯 **Domyślnie**: `small` — najlepszy balans jakości i szybkości dla polskiego.
-
----
+Opcja A — Instalator (zalecana):
-## ⚙️ Ustawienia
-
-Kliknij ⚙️ w oknie VoxFlow, aby otworzyć zaawansowane ustawienia:
-
-| Ustawienie | Opis | Domyślnie |
-|------------|------|-----------|
-| **Model** | Rozmiar modelu Whisper | `small` |
-| **Język** | Auto / PL / EN | `auto` |
-| **Hotkey** | Klawisz hold-to-record | `F2` |
-| **Auto-wpisywanie** | Wpisuj tekst do aktywnego okna | ✅ Włączone |
-| **Auto-kopiowanie** | Kopiuj do schowka | ✅ Włączone |
-| **Metoda wpisywania** | Clipboard (Ctrl+V) / Klawiatura | `clipboard` |
-| **VAD** | Detekcja mowy | ✅ Włączona |
-| **Autokorekta** | Poprawa tekstu | ✅ Włączona |
-| **Dźwięki** | Sygnały start/stop | ✅ Włączone |
-| **Beam size** | Dokładność transkrypcji (1-10) | `5` |
-| **Autostart** | Uruchamiaj z Windows | ❌ Wyłączone |
+```
+BUILD_EXE.bat → tworzy VoxFlow_Setup.exe
+```
----
+Wymaga [Inno Setup 6](https://jrsoftware.org/isinfo.php).
-## 📁 Struktura projektu
+Opcja B — Portable (nie wymaga instalacji):
```
-VoxFlow/
-├── voxflow/
-│ ├── __init__.py # Metadata pakietu
-│ ├── __main__.py # python -m voxflow
-│ ├── main.py # Entry point + test mode
-│ ├── app.py # Główne okno UI (CustomTkinter)
-│ ├── config.py # Zarządzanie konfiguracją
-│ ├── recorder.py # Nagrywanie audio (sounddevice)
-│ ├── transcriber.py # Silnik Whisper (faster-whisper)
-│ ├── post_processor.py # Autokorekta tekstu
-│ ├── hotkey_manager.py # Global hotkey (hold-to-record)
-│ ├── auto_typer.py # Auto-wpisywanie tekstu
-│ ├── overlay.py # Overlay REC na ekranie
-│ ├── tray.py # System tray
-│ ├── sounds.py # Generowanie dźwięków
-│ ├── autostart.py # Autostart Windows (rejestr)
-│ ├── create_shortcut.py # Tworzenie skrótu na pulpicie
-│ └── icon_gen.py # Generowanie ikony .ico
-├── assets/
-│ ├── voxflow.ico # Ikona aplikacji
-│ └── voxflow_256.png # Logo PNG
-├── .github/workflows/
-│ └── ci.yml # GitHub Actions CI
-├── requirements.txt # Zależności Python
-├── pyproject.toml # Konfiguracja pakietu (PEP 621)
-├── START_VOXFLOW.bat # Uruchom jednym kliknięciem
-├── BUILD_EXE.bat # Zbuduj .exe
-├── installer.iss # Instalator Inno Setup
-├── LICENSE # Licencja MIT
-├── CONTRIBUTING.md # Jak kontrybuować
-├── CHANGELOG.md # Historia zmian
-├── SECURITY.md # Polityka bezpieczeństwa
-├── CODE_OF_CONDUCT.md # Kodeks postępowania
-└── README.md # Ten plik
+CREATE_PORTABLE.bat → tworzy dist\VoxFlow_Portable_v1.1.0\
```
----
+Uruchom `START_VOXFLOW.bat` z folderu portable.
-## 🔧 Wymagania systemowe
+---
-- **System**: Windows 10/11 (x64)
-- **Python**: 3.9 lub nowszy
-- **RAM**: 2 GB+ (zależy od modelu)
-- **Dysk**: ~500 MB (model `small`) — ~3 GB (model `large-v3`)
-- **Mikrofon**: dowolny mikrofon USB/wbudowany
-- **Internet**: tylko do pierwszego pobrania modelu; potem **100% offline**
+## ✨ Funkcje
+
+| 🇵🇱 | 🇬🇧 |
+|-----|-----|
+| 🎤 Dyktowanie przytrzymując klawisz | Hold-to-record hotkey |
+| 🌍 Polski + Angielski (auto-detekcja) | Polish + English (auto-detect) |
+| ⚡ 100% lokalne — zero chmury | 100% local — no cloud |
+| 🧠 OpenAI Whisper (faster-whisper) | OpenAI Whisper (faster-whisper) |
+| ✍️ Auto-wpisywanie w aktywne okno | Auto-type into active window |
+| 🎙 Wybór mikrofonu | Microphone device selection |
+| ⌨️ Dowolny klawisz dyktowania | Configurable hotkey |
+| 📋 Auto-kopiowanie do schowka | Auto-copy to clipboard |
+| 🌊 Animacja fal audio podczas nagrywania | Audio waveform animation |
+| 🔲 Ikona w zasobniku | System tray icon |
+| 📚 Historia nagrań | Recording history |
---
-## 🏗️ Budowanie .exe
-
-```bash
-# Zbuduj standalone .exe za pomocą PyInstaller
-BUILD_EXE.bat
+## 🧠 Modele Whisper
-# Gotowy plik: dist/VoxFlow/VoxFlow.exe
-```
+| Model | Rozmiar | Szybkość | Jakość | Dla kogo |
+|-------|---------|----------|--------|----------|
+| `tiny` | ~75 MB | ⚡⚡⚡⚡ | ⭐⭐ | Testy |
+| `base` | ~150 MB | ⚡⚡⚡ | ⭐⭐⭐ | Słaby PC |
+| `small` | ~500 MB | ⚡⚡ | ⭐⭐⭐⭐ | Zalecany |
+| `medium` | ~1.5 GB | ⚡ | ⭐⭐⭐⭐⭐ | Dobry PC |
+| `large-v3` | ~3 GB | 🐢 | ⭐⭐⭐⭐⭐ | GPU |
-Opcjonalnie, jeśli masz zainstalowane [Inno Setup 6](https://jrsoftware.org/isinfo.php), skrypt automatycznie zbuduje też instalator `.exe`.
+> Model pobiera się automatycznie przy pierwszej zmianie (jednorazowo).
---
-## 🔒 Prywatność i bezpieczeństwo
-
-VoxFlow jest **w 100% lokalne**:
+## 🍎 Instalacja macOS
-- ❌ Żadne dane audio nie są wysyłane do internetu
-- ❌ Żadna telemetria nie jest zbierana
-- ❌ Żadne dane użytkownika nie opuszczają komputera
-- ✅ Model AI działa w pełni lokalnie na Twoim CPU
-- ✅ Konfiguracja przechowywana bezpiecznie w `%APPDATA%/VoxFlow/`
+```bash
+chmod +x install_mac.sh
+./install_mac.sh
+```
-Więcej informacji: [SECURITY.md](SECURITY.md)
+> Globalny hotkey wymaga uprawnień Accessibility w System Settings.
+> Szczegóły: [INSTALL_MAC.md](INSTALL_MAC.md)
---
-## 🤝 Współpraca
+## 📜 Licencja
-Chcesz pomóc? Świetnie! Przeczytaj [CONTRIBUTING.md](CONTRIBUTING.md) i otwórz Pull Request.
+MIT License — bezpłatny, open source. Używaj, modyfikuj i dystrybuuj bez ograniczeń.
---
-## 📄 Licencja
+
-Ten projekt jest udostępniony na licencji [MIT](LICENSE).
+AI Evolution Polska — Bezpłatne narzędzia AI dla polskich użytkowników
----
+[](https://github.com/aievolutionpl)
-
- Zbudowane z ❤️ przez AI Evolution Polska
- Open-source tools for the Polish AI community
-
+
diff --git a/SECURITY.md b/SECURITY.md
index 06e59e9..6697148 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -50,4 +50,4 @@ VoxFlow jest **w 100% lokalne**:
---
-*Zbudowane przez [AI Evolution Polska](https://github.com/AI-Evolution-Polska)*
+*Zbudowane przez [AI Evolution Polska](https://github.com/aievolutionpl)*
diff --git a/START_VOXFLOW.bat b/START_VOXFLOW.bat
deleted file mode 100644
index ca50eae..0000000
--- a/START_VOXFLOW.bat
+++ /dev/null
@@ -1,45 +0,0 @@
-@echo off
-title VoxFlow - Local Speech-to-Text (AI Evolution Polska)
-echo.
-echo ╔═══════════════════════════════════════════╗
-echo ║ 🎤 VoxFlow Setup ^& Launch ║
-echo ║ Local Speech-to-Text (PL ^& EN) ║
-echo ║ by AI Evolution Polska ║
-echo ╚═══════════════════════════════════════════╝
-echo.
-
-REM Check if Python is available
-python --version >nul 2>&1
-if %errorlevel% neq 0 (
- echo ❌ Python nie jest zainstalowany!
- echo Pobierz z: https://python.org
- pause
- exit /b 1
-)
-
-REM Check if venv exists, create if not
-if not exist "venv" (
- echo 📦 Tworzenie środowiska wirtualnego...
- python -m venv venv
- if %errorlevel% neq 0 (
- echo ❌ Nie udało się stworzyć venv
- pause
- exit /b 1
- )
-)
-
-REM Activate venv
-call venv\Scripts\activate.bat
-
-REM Install/update dependencies
-echo 📦 Sprawdzanie zależności...
-pip install -r requirements.txt --quiet --disable-pip-version-check 2>nul
-
-REM Launch VoxFlow
-echo.
-echo 🚀 Uruchamiam VoxFlow...
-echo.
-python -m voxflow.main %*
-
-REM Deactivate on exit
-deactivate
diff --git a/VoxFlow.spec b/VoxFlow.spec
new file mode 100644
index 0000000..f27468b
--- /dev/null
+++ b/VoxFlow.spec
@@ -0,0 +1,56 @@
+# -*- mode: python ; coding: utf-8 -*-
+from PyInstaller.utils.hooks import collect_all
+
+datas = [('voxflow', 'voxflow'), ('assets', 'assets')]
+binaries = []
+hiddenimports = ['faster_whisper', 'ctranslate2', 'huggingface_hub', 'tokenizers', 'sounddevice', 'customtkinter', 'pystray', 'keyboard', 'pyperclip', '_sounddevice_data', 'scipy', 'scipy.signal', 'PIL', 'PIL.Image', 'PIL.ImageDraw', 'PIL.ImageFilter']
+tmp_ret = collect_all('customtkinter')
+datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
+tmp_ret = collect_all('faster_whisper')
+datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
+tmp_ret = collect_all('ctranslate2')
+datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2]
+
+
+a = Analysis(
+ ['voxflow\\main.py'],
+ pathex=[],
+ binaries=binaries,
+ datas=datas,
+ hiddenimports=hiddenimports,
+ hookspath=[],
+ hooksconfig={},
+ runtime_hooks=[],
+ excludes=[],
+ noarchive=False,
+ optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+ pyz,
+ a.scripts,
+ [],
+ exclude_binaries=True,
+ name='VoxFlow',
+ 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=['assets\\voxflow.ico'],
+)
+coll = COLLECT(
+ exe,
+ a.binaries,
+ a.datas,
+ strip=False,
+ upx=True,
+ upx_exclude=[],
+ name='VoxFlow',
+)
diff --git a/assets/voxflow.ico b/assets/voxflow.ico
index 9f3a19d..997d890 100644
Binary files a/assets/voxflow.ico and b/assets/voxflow.ico differ
diff --git a/assets/voxflow_256.png b/assets/voxflow_256.png
index f51c940..389e5ff 100644
Binary files a/assets/voxflow_256.png and b/assets/voxflow_256.png differ
diff --git a/install.bat b/install.bat
new file mode 100644
index 0000000..bc80883
--- /dev/null
+++ b/install.bat
@@ -0,0 +1,138 @@
+@echo off
+chcp 65001 >nul
+cd /d "%~dp0"
+title VoxFlow — Instalacja (AI Evolution Polska)
+echo.
+echo ╔══════════════════════════════════════════════════════╗
+echo ║ VoxFlow — Instalacja jednym kliknięciem ║
+echo ║ by AI Evolution Polska ^| Open Source ║
+echo ╚══════════════════════════════════════════════════════╝
+echo.
+
+REM ─── Check Python ───────────────────────────────────────────────
+python --version >nul 2>&1
+if %errorlevel% neq 0 (
+ echo ❌ Python nie jest zainstalowany!
+ echo.
+ echo Pobierz Python 3.10+ ze strony:
+ echo https://www.python.org/downloads/
+ echo.
+ echo WAŻNE: Podczas instalacji zaznacz opcję "Add Python to PATH"
+ echo.
+ pause
+ start https://www.python.org/downloads/
+ exit /b 1
+)
+
+for /f "tokens=2" %%v in ('python --version 2^>^&1') do set PY_VERSION=%%v
+echo ✅ Python %PY_VERSION% znaleziony
+
+REM ─── Check Python version (need 3.9+) ───────────────────────────
+for /f "tokens=1,2 delims=." %%a in ("%PY_VERSION%") do (
+ set PY_MAJOR=%%a
+ set PY_MINOR=%%b
+)
+if %PY_MAJOR% LSS 3 (
+ echo ❌ Wymagany Python 3.9+, znaleziono %PY_VERSION%
+ pause
+ exit /b 1
+)
+if %PY_MAJOR%==3 if %PY_MINOR% LSS 9 (
+ echo ❌ Wymagany Python 3.9+, znaleziono %PY_VERSION%
+ pause
+ exit /b 1
+)
+
+echo.
+echo ═══════════════════════════════════════════════════════
+echo KROK 1: Tworzenie środowiska wirtualnego (venv)...
+echo ═══════════════════════════════════════════════════════
+echo.
+
+if exist venv (
+ echo ℹ️ Środowisko venv już istnieje — pomijam tworzenie.
+) else (
+ python -m venv venv
+ if %errorlevel% neq 0 (
+ echo ❌ Nie udało się stworzyć venv!
+ pause
+ exit /b 1
+ )
+ echo ✅ Środowisko venv stworzone.
+)
+
+echo.
+echo ═══════════════════════════════════════════════════════
+echo KROK 2: Instalacja wymaganych pakietów...
+echo ═══════════════════════════════════════════════════════
+echo.
+
+call venv\Scripts\activate.bat
+
+pip install --upgrade pip --quiet --disable-pip-version-check
+pip install -r requirements.txt --quiet --disable-pip-version-check
+
+if %errorlevel% neq 0 (
+ echo.
+ echo ❌ Instalacja pakietów nie powiodła się!
+ echo.
+ echo Spróbuj ręcznie uruchomić:
+ echo venv\Scripts\activate
+ echo pip install -r requirements.txt
+ echo.
+ pause
+ exit /b 1
+)
+
+echo ✅ Wszystkie pakiety zainstalowane!
+
+echo.
+echo ═══════════════════════════════════════════════════════
+echo KROK 3: Weryfikacja instalacji...
+echo ═══════════════════════════════════════════════════════
+echo.
+
+python -m voxflow.main --test
+
+if %errorlevel% neq 0 (
+ echo.
+ echo ⚠️ Weryfikacja nie powiodła się. Sprawdź komunikaty błędów powyżej.
+ echo.
+ pause
+ exit /b 1
+)
+
+echo.
+echo ═══════════════════════════════════════════════════════
+echo ✅ VoxFlow zainstalowany pomyślnie!
+
+REM ─── Stwórz START_VOXFLOW.bat ──────────────────────────────
+echo @echo off > START_VOXFLOW.bat
+echo chcp 65001 ^>nul >> START_VOXFLOW.bat
+echo cd /d "%%~dp0" >> START_VOXFLOW.bat
+echo call venv\Scripts\activate.bat >> START_VOXFLOW.bat
+echo start "" venv\Scripts\pythonw.exe -m voxflow.main >> START_VOXFLOW.bat
+
+echo.
+echo ═══════════════════════════════════════════════════════
+echo ✅ Instalacja zakończona!
+echo.
+echo Uruchom aplikację:
+echo 👉 Kliknij dwukrotnie START_VOXFLOW.bat
+echo.
+echo Lub ręcznie:
+echo venv\Scripts\activate
+echo python -m voxflow.main
+echo ═══════════════════════════════════════════════════════
+echo.
+
+set /p LAUNCH="Czy uruchomić VoxFlow teraz? [T/n]: "
+if /i "%LAUNCH%"=="n" goto :end
+
+echo.
+echo 🚀 Uruchamianie VoxFlow...
+start "" venv\Scripts\pythonw.exe -m voxflow.main
+:end
+deactivate
+echo.
+pause
diff --git a/install_mac.sh b/install_mac.sh
new file mode 100644
index 0000000..2678171
--- /dev/null
+++ b/install_mac.sh
@@ -0,0 +1,190 @@
+#!/bin/bash
+# VoxFlow — Instalacja macOS (Intel & Apple Silicon)
+# by AI Evolution Polska | Open Source
+# ─────────────────────────────────────────────────────────────────
+
+set -e
+
+RED='\033[0;31m'
+GREEN='\033[0;32m'
+YELLOW='\033[1;33m'
+CYAN='\033[0;36m'
+BOLD='\033[1m'
+NC='\033[0m'
+
+echo ""
+echo -e "${CYAN}${BOLD}"
+echo " ╔══════════════════════════════════════════════════════╗"
+echo " ║ VoxFlow — Instalacja macOS ║"
+echo " ║ by AI Evolution Polska | Open Source ║"
+echo " ╚══════════════════════════════════════════════════════╝"
+echo -e "${NC}"
+echo ""
+
+# ─── Check macOS ────────────────────────────────────────────────
+if [[ "$OSTYPE" != "darwin"* ]]; then
+ echo -e "${RED}❌ Ten skrypt jest przeznaczony dla macOS.${NC}"
+ echo " Dla Windows użyj install.bat"
+ exit 1
+fi
+
+ARCH=$(uname -m)
+echo -e " ${CYAN}Architektura:${NC} $ARCH"
+if [[ "$ARCH" == "arm64" ]]; then
+ echo -e " ${GREEN}✅ Apple Silicon (M1/M2/M3) wykryty${NC}"
+else
+ echo -e " ${GREEN}✅ Intel Mac wykryty${NC}"
+fi
+
+# ─── Check Homebrew ─────────────────────────────────────────────
+echo ""
+echo "══════════════════════════════════════════════════════"
+echo " KROK 1: Sprawdzanie Homebrew..."
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+if ! command -v brew &>/dev/null; then
+ echo -e "${YELLOW} ℹ️ Homebrew nie znaleziony. Instaluję...${NC}"
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
+
+ # Add brew to PATH for Apple Silicon
+ if [[ "$ARCH" == "arm64" ]]; then
+ echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$HOME/.zprofile"
+ eval "$(/opt/homebrew/bin/brew shellenv)"
+ fi
+ echo -e "${GREEN} ✅ Homebrew zainstalowany${NC}"
+else
+ echo -e "${GREEN} ✅ Homebrew już zainstalowany${NC}"
+fi
+
+# ─── Install PortAudio (needed by sounddevice) ──────────────────
+echo ""
+echo "══════════════════════════════════════════════════════"
+echo " KROK 2: Instalacja bibliotek audio (portaudio)..."
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+if brew list portaudio &>/dev/null; then
+ echo -e "${GREEN} ✅ portaudio już zainstalowany${NC}"
+else
+ brew install portaudio
+ echo -e "${GREEN} ✅ portaudio zainstalowany${NC}"
+fi
+
+# ─── Check Python 3.9+ ─────────────────────────────────────────
+echo ""
+echo "══════════════════════════════════════════════════════"
+echo " KROK 3: Sprawdzanie Python 3.9+..."
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+PYTHON_CMD=""
+for cmd in python3.12 python3.11 python3.10 python3.9 python3; do
+ if command -v $cmd &>/dev/null; then
+ VERSION=$($cmd --version 2>&1 | awk '{print $2}')
+ MAJOR=$(echo $VERSION | cut -d. -f1)
+ MINOR=$(echo $VERSION | cut -d. -f2)
+ if [[ $MAJOR -eq 3 && $MINOR -ge 9 ]]; then
+ PYTHON_CMD=$cmd
+ echo -e "${GREEN} ✅ Python $VERSION — używam $cmd${NC}"
+ break
+ fi
+ fi
+done
+
+if [[ -z "$PYTHON_CMD" ]]; then
+ echo -e "${YELLOW} ℹ️ Python 3.9+ nie znaleziony. Instaluję przez Homebrew...${NC}"
+ brew install python@3.11
+ PYTHON_CMD="python3.11"
+ echo -e "${GREEN} ✅ Python 3.11 zainstalowany${NC}"
+fi
+
+# ─── Create venv ────────────────────────────────────────────────
+echo ""
+echo "══════════════════════════════════════════════════════"
+echo " KROK 4: Tworzenie środowiska wirtualnego..."
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+
+if [[ -d venv ]]; then
+ echo -e " ${CYAN}ℹ️ Środowisko venv już istnieje${NC}"
+else
+ $PYTHON_CMD -m venv venv
+ echo -e "${GREEN} ✅ Środowisko venv stworzone${NC}"
+fi
+
+source venv/bin/activate
+
+# ─── Install requirements ────────────────────────────────────────
+echo ""
+echo "══════════════════════════════════════════════════════"
+echo " KROK 5: Instalacja wymaganych pakietów..."
+echo " (pierwsze uruchomienie może potrwać kilka minut)"
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+pip install --upgrade pip --quiet
+pip install -r requirements.txt --quiet
+
+echo -e "${GREEN} ✅ Pakiety zainstalowane${NC}"
+
+# ─── Verify ─────────────────────────────────────────────────────
+echo ""
+echo "══════════════════════════════════════════════════════"
+echo " KROK 6: Weryfikacja instalacji..."
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+python -m voxflow.main --test
+
+# ─── Create launcher ─────────────────────────────────────────────
+echo ""
+echo "══════════════════════════════════════════════════════"
+echo " KROK 7: Tworzenie skrótu uruchamiania..."
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+LAUNCHER="$SCRIPT_DIR/VoxFlow.command"
+cat > "$LAUNCHER" << 'EOF'
+#!/bin/bash
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$SCRIPT_DIR"
+source venv/bin/activate
+python -m voxflow.main
+EOF
+chmod +x "$LAUNCHER"
+echo -e "${GREEN} ✅ Launcher VoxFlow.command stworzony${NC}"
+
+# ─── Accessibility warning (macOS keyboard hook) ─────────────────
+echo ""
+echo -e "${YELLOW} ⚠️ WAŻNA INFORMACJA — Uprawnienia Accessibility:${NC}"
+echo ""
+echo " Aby globalny hotkey (np. F2) działał we WSZYSTKICH aplikacjach,"
+echo " musisz nadać VoxFlow uprawnienia Accessibility:"
+echo ""
+echo " 1. Otwórz: Apple Menu → System Settings → Privacy & Security"
+echo " 2. Kliknij: Accessibility"
+echo " 3. Dodaj Terminal lub VoxFlow.command do listy"
+echo ""
+echo " Bez tych uprawnień: hotkey działa tylko gdy VoxFlow jest na pierwszym planie."
+echo ""
+
+# ─── Done ────────────────────────────────────────────────────────
+echo "══════════════════════════════════════════════════════"
+echo -e "${GREEN}${BOLD} ✅ VoxFlow zainstalowany pomyślnie!${NC}"
+echo ""
+echo " Uruchom aplikację:"
+echo " • Kliknij dwukrotnie: VoxFlow.command"
+echo " • lub w terminalu: ./VoxFlow.command"
+echo "══════════════════════════════════════════════════════"
+echo ""
+
+read -p " Uruchomić VoxFlow teraz? [T/n]: " LAUNCH
+if [[ "$LAUNCH" != "n" && "$LAUNCH" != "N" ]]; then
+ echo ""
+ echo -e " ${CYAN}🚀 Uruchamianie VoxFlow...${NC}"
+ python -m voxflow.main &
+fi
diff --git a/installer.iss b/installer.iss
index 6747072..2979d3b 100644
--- a/installer.iss
+++ b/installer.iss
@@ -1,48 +1,134 @@
-; VoxFlow Installer Script for Inno Setup
-; Requires Inno Setup 6+ (https://jrsoftware.org/isinfo.php)
+; ═══════════════════════════════════════════════════════════════
+; VoxFlow — Inno Setup Installer Script
+; Requires Inno Setup 6.3+ (https://jrsoftware.org/isinfo.php)
+; by AI Evolution Polska | Open Source
+; ═══════════════════════════════════════════════════════════════
-#define MyAppName "VoxFlow"
-#define MyAppVersion "1.0.0"
+#define MyAppName "VoxFlow"
+#define MyAppVersion "1.1.0"
#define MyAppPublisher "AI Evolution Polska"
-#define MyAppExeName "VoxFlow.exe"
-#define MyAppURL "https://github.com/AI-Evolution-Polska/VoxFlow"
+#define MyAppExeName "VoxFlow.exe"
+#define MyAppURL "https://github.com/aievolutionpl/VoxFlow"
[Setup]
+; Unique GUID - do not change after first release
AppId={{B8E4F2A1-3C7D-4E9F-A5B6-1D2E3F4A5B6C}
+
AppName={#MyAppName}
AppVersion={#MyAppVersion}
+AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}/issues
+AppUpdatesURL={#MyAppURL}/releases
+AppCopyright=Copyright © 2025 AI Evolution Polska
+
+; Installation directory
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
DisableProgramGroupPage=yes
+PrivilegesRequired=lowest
+PrivilegesRequiredOverridesAllowed=dialog
+
+; Output
OutputDir=installer_output
OutputBaseFilename=VoxFlow_Setup_v{#MyAppVersion}
SetupIconFile=assets\voxflow.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
-Compression=lzma2
+UninstallDisplayName={#MyAppName}
+
+; Compression
+Compression=lzma2/ultra64
SolidCompression=yes
+LZMAUseSeparateProcess=yes
+LZMANumBlockThreads=4
+
+; Visual style
WizardStyle=modern
+WizardResizable=no
+WizardSizePercent=100
+
+; Windows version requirement
+MinVersion=10.0
ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
+; Show "This will take a moment" during extraction
+ShowLanguageDialog=auto
+
+; Restart behavior
+CloseApplications=yes
+RestartApplications=no
+
[Languages]
-Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
+Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"
+; ─── Welcome page text ────────────────────────────────────────────
+[Messages]
+polish.WelcomeLabel1=Witaj w kreatorze instalacji programu [name]
+polish.WelcomeLabel2=Program [name] to bezpłatne narzędzie do dyktowania głosem na komputer.%n%nDziała w 100%% offline — bez chmury, bez subskrypcji.%n%nZalecane jest zamknięcie wszystkich innych aplikacji przed kontynuowaniem.
+english.WelcomeLabel1=Welcome to the [name] Setup Wizard
+english.WelcomeLabel2=[name] is a free, open-source voice dictation tool for your PC.%n%nWorks 100%% offline — no cloud, no subscription required.%n%nIt is recommended that you close all other applications before continuing.
+
+[CustomMessages]
+polish.CreateDesktopShortcut=Utwórz skrót na pulpicie
+polish.RunAtStartup=Uruchamiaj VoxFlow razem z Windows (zalecane)
+polish.PostInstallInfo=VoxFlow został pomyślnie zainstalowany!%n%nPrzy pierwszym uruchomieniu aplikacja pobierze model AI (~500 MB). Wymagane jednorazowe połączenie z internetem.
+english.CreateDesktopShortcut=Create a desktop shortcut
+english.RunAtStartup=Launch VoxFlow with Windows (recommended)
+english.PostInstallInfo=VoxFlow has been successfully installed!%n%nOn first launch, the app will download the AI model (~500 MB). A one-time internet connection is required.
+
[Tasks]
-Name: "desktopicon"; Description: "Utwórz ikonę na pulpicie"; GroupDescription: "Dodatkowe ikony:"; Flags: unchecked
-Name: "startupicon"; Description: "Uruchamiaj VoxFlow razem z Windows"; GroupDescription: "Autostart:"; Flags: unchecked
+Name: "desktopicon"; Description: "{cm:CreateDesktopShortcut}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: checked
+Name: "startupentry"; Description: "{cm:RunAtStartup}"; GroupDescription: "Autostart:"; Flags: unchecked
[Files]
+; Main application files (built by PyInstaller)
Source: "dist\VoxFlow\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
-Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
+; Start menu
+Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"
Name: "{group}\Odinstaluj {#MyAppName}"; Filename: "{uninstallexe}"
-Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
-Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: startupicon
+
+; Desktop shortcut (optional)
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon
+
+; Startup (optional)
+Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: startupentry
+
+[Registry]
+; Register in "Add/Remove Programs" with additional info
+Root: HKCU; Subkey: "Software\AI Evolution Polska\VoxFlow"; ValueType: string; ValueName: "InstallPath"; ValueData: "{app}"; Flags: uninsdeletekey
+Root: HKCU; Subkey: "Software\AI Evolution Polska\VoxFlow"; ValueType: string; ValueName: "Version"; ValueData: "{#MyAppVersion}"
[Run]
-Filename: "{app}\{#MyAppExeName}"; Description: "Uruchom VoxFlow"; Flags: nowait postinstall skipifsilent
+; Offer to launch after install
+Filename: "{app}\{#MyAppExeName}"; Description: "Uruchom VoxFlow teraz"; Flags: nowait postinstall skipifsilent
+
+[UninstallDelete]
+; Remove config folder on uninstall (optional — comment out to keep user settings)
+; Type: filesandordirs; Name: "{userappdata}\VoxFlow"
+
+[Code]
+// ─── Check Windows version ─────────────────────────────────────
+function InitializeSetup(): Boolean;
+begin
+ Result := True;
+ if not IsWin64 then
+ begin
+ MsgBox('VoxFlow wymaga 64-bitowej wersji systemu Windows.' + #13#10 +
+ 'VoxFlow requires 64-bit Windows.', mbError, MB_OK);
+ Result := False;
+ end;
+end;
+
+// ─── Show info after install ────────────────────────────────────
+procedure CurStepChanged(CurStep: TSetupStep);
+begin
+ if CurStep = ssPostInstall then
+ begin
+ MsgBox(CustomMessage('PostInstallInfo'), mbInformation, MB_OK);
+ end;
+end;
diff --git a/pyproject.toml b/pyproject.toml
index 0930968..1a7defd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,10 +41,10 @@ dependencies = [
]
[project.urls]
-Homepage = "https://github.com/AI-Evolution-Polska/VoxFlow"
-Repository = "https://github.com/AI-Evolution-Polska/VoxFlow"
-Issues = "https://github.com/AI-Evolution-Polska/VoxFlow/issues"
-Changelog = "https://github.com/AI-Evolution-Polska/VoxFlow/blob/main/CHANGELOG.md"
+Homepage = "https://github.com/aievolutionpl/VoxFlow"
+Repository = "https://github.com/aievolutionpl/VoxFlow"
+Issues = "https://github.com/aievolutionpl/VoxFlow/issues"
+Changelog = "https://github.com/aievolutionpl/VoxFlow/blob/main/CHANGELOG.md"
[project.scripts]
voxflow = "voxflow.main:main"
diff --git a/voxflow/__init__.py b/voxflow/__init__.py
index d89aeea..91fa437 100644
--- a/voxflow/__init__.py
+++ b/voxflow/__init__.py
@@ -1,10 +1,10 @@
"""VoxFlow - Local Speech-to-Text Application
Built by AI Evolution Polska
-https://github.com/AI-Evolution-Polska/VoxFlow
+https://github.com/aievolutionpl/VoxFlow
"""
-__version__ = "1.0.0"
+__version__ = "1.1.0"
__app_name__ = "VoxFlow"
__author__ = "AI Evolution Polska"
__license__ = "MIT"
-__url__ = "https://github.com/AI-Evolution-Polska/VoxFlow"
+__url__ = "https://github.com/aievolutionpl/VoxFlow"
diff --git a/voxflow/app.py b/voxflow/app.py
index 14197b5..5c5c6d5 100644
--- a/voxflow/app.py
+++ b/voxflow/app.py
@@ -1,7 +1,7 @@
-"""VoxFlow Main Application - Beautiful Desktop UI with Hold-to-Record.
+"""VoxFlow Main Application - Premium Desktop UI with Hold-to-Record.
Built by AI Evolution Polska
-https://github.com/AI-Evolution-Polska/VoxFlow
+https://github.com/aievolutionpl/VoxFlow
"""
import threading
import time
@@ -16,31 +16,47 @@
from voxflow.recorder import AudioRecorder
from voxflow.transcriber import VoxTranscriber
from voxflow.hotkey_manager import HotkeyManager
-from voxflow.tray import TrayManager
from voxflow.auto_typer import AutoTyper
from voxflow import sounds
from voxflow.overlay import RecordingOverlay
-from voxflow.autostart import set_autostart, is_autostart_enabled
from voxflow import __version__, __author__
+# Optional modules — gracefully degrade if unavailable
+try:
+ from voxflow.tray import TrayManager
+ _TRAY_AVAILABLE = True
+except Exception:
+ _TRAY_AVAILABLE = False
+
+try:
+ from voxflow.autostart import set_autostart, is_autostart_enabled
+ _AUTOSTART_AVAILABLE = True
+except Exception:
+ _AUTOSTART_AVAILABLE = False
+ def set_autostart(_enabled: bool) -> None: pass
+ def is_autostart_enabled() -> bool: return False
+
# ─── Color Palette ────────────────────────────────────────────────────────────
C = {
- "bg": "#0c0c18",
- "bg_card": "#161630",
- "bg_hover": "#1e1e42",
- "bg_input": "#12122a",
- "accent": "#8b5cf6",
+ "bg": "#09091a",
+ "bg_card": "#12122b",
+ "bg_hover": "#1a1a3a",
+ "bg_input": "#0e0e22",
+ "accent": "#7c3aed",
"accent2": "#a78bfa",
- "accent_dim": "#6d28d9",
+ "accent3": "#c4b5fd",
+ "accent_dim": "#5b21b6",
+ "accent_glow": "#4c1d95",
"rec_red": "#ef4444",
"rec_glow": "#dc2626",
"ok": "#22c55e",
"warn": "#f59e0b",
"txt": "#f1f5f9",
"txt2": "#94a3b8",
- "txt3": "#64748b",
- "border": "#2a2a50",
+ "txt3": "#475569",
+ "border": "#1e1e42",
+ "border2": "#2d2d5e",
}
@@ -56,7 +72,7 @@ def __init__(self):
self.title("VoxFlow")
self.geometry(f"{self.config.window_width}x{self.config.window_height}")
- self.minsize(440, 620)
+ self.minsize(460, 700)
self.configure(fg_color=C["bg"])
# ─── State ────────────────────────────────────────────────
@@ -67,6 +83,8 @@ def __init__(self):
self._history: list[dict] = []
self._alive = True
self._settings_visible = False
+ self._capturing_hotkey = False
+ self._audio_devices: list[dict] = []
# ─── Engine ───────────────────────────────────────────────
self.recorder = AudioRecorder(
@@ -75,7 +93,9 @@ def __init__(self):
silence_threshold=self.config.silence_threshold,
silence_duration=self.config.silence_duration,
max_duration=self.config.max_recording_duration,
+ device_index=self.config.audio_device_index,
on_level_change=self._on_level,
+ on_device_fallback=self._on_device_fallback,
)
self.transcriber = VoxTranscriber(
@@ -90,33 +110,74 @@ def __init__(self):
on_release=self._on_hotkey_release,
)
- self.tray = TrayManager(
- on_show=self._show,
- on_toggle_recording=lambda: self.after(0, self._toggle_recording),
- on_quit=self._quit,
- )
+ if _TRAY_AVAILABLE:
+ self.tray = TrayManager(
+ on_show=self._show,
+ on_toggle_recording=lambda: self.after(0, self._toggle_recording),
+ on_quit=self._quit,
+ )
+ else:
+ self.tray = None
self.auto_typer = AutoTyper()
self.overlay = RecordingOverlay()
# ─── Build ────────────────────────────────────────────────
+ self._load_audio_devices()
self._build_ui()
self._start_services()
self.protocol("WM_DELETE_WINDOW", self._on_close)
+ self.bind("", self._on_window_configure)
self._animate()
# ═══════════════════════════════════════════════════════════════
- # UI
+ # DEVICE ENUMERATION
+ # ═══════════════════════════════════════════════════════════════
+
+ def _load_audio_devices(self):
+ """Load list of available audio input devices."""
+ try:
+ self._audio_devices = AudioRecorder.list_devices()
+ except Exception:
+ self._audio_devices = []
+
+ def _get_device_names(self) -> list[str]:
+ """Return device display names for UI dropdown."""
+ names = ["🎤 Domyślny mikrofon"]
+ for d in self._audio_devices:
+ name = d["name"]
+ if len(name) > 38:
+ name = name[:35] + "..."
+ names.append(f"[{d['index']}] {name}")
+ return names
+
+ def _get_current_device_label(self) -> str:
+ """Get label for the currently selected device."""
+ idx = self.config.audio_device_index
+ if idx < 0:
+ return "🎤 Domyślny mikrofon"
+ for d in self._audio_devices:
+ if d["index"] == idx:
+ name = d["name"]
+ if len(name) > 38:
+ name = name[:35] + "..."
+ return f"[{idx}] {name}"
+ return "🎤 Domyślny mikrofon"
+
+ # ═══════════════════════════════════════════════════════════════
+ # UI BUILDER
# ═══════════════════════════════════════════════════════════════
def _build_ui(self):
+ # Outer scrollable container
self.main = ctk.CTkFrame(self, fg_color="transparent")
- self.main.pack(fill="both", expand=True, padx=20, pady=10)
+ self.main.pack(fill="both", expand=True, padx=18, pady=(8, 6))
self._build_header()
self._build_record_btn()
self._build_level_bar()
+ self._build_mic_selector()
self._build_status()
self._build_transcript()
self._build_quick_controls()
@@ -124,43 +185,76 @@ def _build_ui(self):
self._build_settings_panel()
self._build_footer()
+ # ── Header ───────────────────────────────────────────────────
+
def _build_header(self):
f = ctk.CTkFrame(self.main, fg_color="transparent")
- f.pack(fill="x", pady=(8, 2))
+ f.pack(fill="x", pady=(6, 4))
row = ctk.CTkFrame(f, fg_color="transparent")
row.pack(fill="x")
- ctk.CTkLabel(row, text="🎤 VoxFlow",
- font=ctk.CTkFont(family="Segoe UI", size=30, weight="bold"),
- text_color=C["txt"]).pack(side="left")
+ # Logo + title
+ logo_frame = ctk.CTkFrame(row, fg_color="transparent")
+ logo_frame.pack(side="left")
+
+ ctk.CTkLabel(
+ logo_frame, text="🎤",
+ font=ctk.CTkFont(size=28),
+ ).pack(side="left", padx=(0, 6))
+
+ ctk.CTkLabel(
+ logo_frame, text="VoxFlow",
+ font=ctk.CTkFont(family="Segoe UI", size=26, weight="bold"),
+ text_color=C["txt"],
+ ).pack(side="left")
+
+ ctk.CTkLabel(
+ logo_frame, text=f"v{__version__}",
+ font=ctk.CTkFont(size=10),
+ text_color=C["accent3"],
+ fg_color=C["bg_card"],
+ corner_radius=6,
+ padx=6, pady=1,
+ ).pack(side="left", padx=(8, 0), pady=(4, 0))
+
+ # Right buttons
+ btn_frame = ctk.CTkFrame(row, fg_color="transparent")
+ btn_frame.pack(side="right")
- # Version badge
- ctk.CTkLabel(row, text=f"v{__version__}",
- font=ctk.CTkFont(size=10),
- text_color=C["accent2"],
- fg_color=C["bg_card"],
- corner_radius=6,
- padx=6, pady=1).pack(side="left", padx=(8, 0))
-
- # Settings gear button
self.settings_btn = ctk.CTkButton(
- row, text="⚙️", width=36, height=36,
- font=ctk.CTkFont(size=16),
- fg_color=C["bg_card"], hover_color=C["accent"],
+ btn_frame, text="⚙", width=34, height=34,
+ font=ctk.CTkFont(size=15),
+ fg_color=C["bg_card"], hover_color=C["accent_dim"],
+ border_width=1, border_color=C["border2"],
corner_radius=10, command=self._toggle_settings,
)
self.settings_btn.pack(side="right")
- ctk.CTkLabel(f, text="Przytrzymaj F2 aby dyktować • 100% offline",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(anchor="w", pady=(2, 0))
+ # Subtitle
+ hk_display = self.config.hotkey.upper().replace("+", " + ")
+ self.subtitle_label = ctk.CTkLabel(
+ f,
+ text=f"Przytrzymaj {hk_display} aby dyktować • 100% offline",
+ font=ctk.CTkFont(family="Segoe UI", size=11),
+ text_color=C["txt3"],
+ )
+ self.subtitle_label.pack(anchor="w", pady=(2, 0))
+
+ # Thin separator line
+ ctk.CTkFrame(self.main, fg_color=C["border"], height=1).pack(fill="x", pady=(4, 0))
+
+ # ── Record Button (canvas) ────────────────────────────────────
def _build_record_btn(self):
f = ctk.CTkFrame(self.main, fg_color="transparent")
- f.pack(fill="x", pady=12)
+ f.pack(fill="x", pady=(14, 4))
- sz = 150
- self.canvas = ctk.CTkCanvas(f, width=sz, height=sz, bg=C["bg"], highlightthickness=0)
+ sz = 160
+ self.canvas = ctk.CTkCanvas(
+ f, width=sz, height=sz,
+ bg=C["bg"], highlightthickness=0,
+ )
self.canvas.pack()
self.canvas.bind("", lambda e: self._start_rec())
self.canvas.bind("", lambda e: self._stop_rec())
@@ -170,301 +264,410 @@ def _build_record_btn(self):
def _draw_btn(self):
self.canvas.delete("all")
cx = cy = self._csz // 2
- r = 52
+ r = 55
if self._recording:
- # Glow rings
+ # Outer glow rings
for i in range(3):
- p = self._phase + i * 0.8
- er = r + 10 + i * 11 + ((math.sin(p) + 1) / 2) * 8
- self.canvas.create_oval(cx-er, cy-er, cx+er, cy+er,
- fill="", outline=C["rec_red"], width=max(1, 3-i))
- self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r,
- fill=C["rec_red"], outline=C["rec_glow"], width=3)
+ p = self._phase + i * 0.9
+ er = r + 12 + i * 12 + ((math.sin(p) + 1) / 2) * 8
+ alpha_hex = ["40", "28", "18"][i]
+ self.canvas.create_oval(
+ cx - er, cy - er, cx + er, cy + er,
+ fill="", outline=C["rec_red"], width=max(1, 3 - i)
+ )
+ # Main circle
+ self.canvas.create_oval(
+ cx - r, cy - r, cx + r, cy + r,
+ fill=C["rec_red"], outline="#b91c1c", width=3,
+ )
# Waveform bars
- for i in range(7):
- bx = cx - 24 + i * 8
- amp = abs(math.sin(self._phase * 2 + i * 0.7)) * 18 + 4
- self.canvas.create_rectangle(bx-2, cy-amp, bx+2, cy+amp,
- fill="white", outline="")
+ for i in range(9):
+ bx = cx - 28 + i * 7
+ amp = abs(math.sin(self._phase * 2 + i * 0.65)) * 20 + 5
+ lvl_boost = self._level * 15
+ amp = min(amp + lvl_boost, 26)
+ self.canvas.create_rectangle(
+ bx - 2, cy - amp, bx + 2, cy + amp,
+ fill="white", outline="",
+ )
+
elif self._processing:
- self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r,
- fill=C["warn"], outline="#d97706", width=3)
+ # Processing spinner
+ self.canvas.create_oval(
+ cx - r, cy - r, cx + r, cy + r,
+ fill=C["warn"], outline="#b45309", width=3,
+ )
for i in range(8):
a = self._phase * 3 + i * (math.pi / 4)
- dx, dy = cx + math.cos(a) * 25, cy + math.sin(a) * 25
- self.canvas.create_oval(dx-4, dy-4, dx+4, dy+4, fill="white", outline="")
+ dx = cx + math.cos(a) * 28
+ dy = cy + math.sin(a) * 28
+ size = 5 - i * 0.3
+ self.canvas.create_oval(
+ dx - size, dy - size, dx + size, dy + size,
+ fill="white", outline="",
+ )
+ self.canvas.create_text(
+ cx, cy, text="AI",
+ fill="white",
+ font=("Segoe UI", 13, "bold"),
+ )
+
else:
- gf = (math.sin(self._phase * 0.4) + 1) / 2
- gr = r + 4 + gf * 5
- self.canvas.create_oval(cx-gr, cy-gr, cx+gr, cy+gr,
- fill="", outline=C["accent"], width=2)
- self.canvas.create_oval(cx-r, cy-r, cx+r, cy+r,
- fill=C["accent"], outline=C["accent2"], width=3)
- # Mic icon
- self.canvas.create_oval(cx-10, cy-24, cx+10, cy+0, fill="white", outline="")
- self.canvas.create_line(cx, cy, cx, cy+14, fill="white", width=3)
- self.canvas.create_line(cx-10, cy+14, cx+10, cy+14, fill="white", width=3)
- self.canvas.create_arc(cx-16, cy-12, cx+16, cy+8,
- start=200, extent=140, style="arc", outline="white", width=2)
+ # Idle — gentle pulse
+ gf = (math.sin(self._phase * 0.35) + 1) / 2
+ # Outer glow
+ gr = r + 6 + gf * 6
+ self.canvas.create_oval(
+ cx - gr, cy - gr, cx + gr, cy + gr,
+ fill="", outline=C["accent"], width=2,
+ )
+ # Second glow ring
+ gr2 = r + 2 + gf * 3
+ self.canvas.create_oval(
+ cx - gr2, cy - gr2, cx + gr2, cy + gr2,
+ fill="", outline=C["accent2"], width=1,
+ )
+ # Main circle
+ self.canvas.create_oval(
+ cx - r, cy - r, cx + r, cy + r,
+ fill=C["accent"], outline=C["accent2"], width=3,
+ )
+ # Mic body
+ self.canvas.create_oval(cx - 11, cy - 26, cx + 11, cy + 2, fill="white", outline="")
+ # Mic stand
+ self.canvas.create_line(cx, cy + 2, cx, cy + 16, fill="white", width=3)
+ self.canvas.create_line(cx - 11, cy + 16, cx + 11, cy + 16, fill="white", width=3)
+ # Mic arc
+ self.canvas.create_arc(
+ cx - 18, cy - 14, cx + 18, cy + 10,
+ start=200, extent=140, style="arc", outline="white", width=2,
+ )
+
+ # ── Level Bar ─────────────────────────────────────────────────
def _build_level_bar(self):
f = ctk.CTkFrame(self.main, fg_color="transparent")
- f.pack(fill="x", pady=(0, 4))
- self.level_bar = ctk.CTkProgressBar(f, height=5, corner_radius=3,
- fg_color=C["bg_card"], progress_color=C["accent"], border_width=0)
- self.level_bar.pack(fill="x", padx=25)
+ f.pack(fill="x", pady=(0, 2))
+ self.level_bar = ctk.CTkProgressBar(
+ f, height=4, corner_radius=2,
+ fg_color=C["bg_card"],
+ progress_color=C["accent"],
+ border_width=0,
+ )
+ self.level_bar.pack(fill="x", padx=30)
self.level_bar.set(0)
+ # ── Microphone Selector ───────────────────────────────────────
+
+ def _build_mic_selector(self):
+ card = ctk.CTkFrame(
+ self.main, fg_color=C["bg_card"],
+ corner_radius=10,
+ border_width=1, border_color=C["border"],
+ )
+ card.pack(fill="x", pady=(6, 4))
+
+ inner = ctk.CTkFrame(card, fg_color="transparent")
+ inner.pack(fill="x", padx=12, pady=8)
+
+ ctk.CTkLabel(
+ inner, text="🎙 Mikrofon",
+ font=ctk.CTkFont(size=11, weight="bold"),
+ text_color=C["txt2"],
+ ).pack(side="left", padx=(0, 8))
+
+ device_names = self._get_device_names()
+ current = self._get_current_device_label()
+ if current not in device_names:
+ current = device_names[0]
+
+ self.mic_var = ctk.StringVar(value=current)
+ self.mic_menu = ctk.CTkOptionMenu(
+ inner,
+ values=device_names,
+ variable=self.mic_var,
+ font=ctk.CTkFont(size=11),
+ fg_color=C["bg_input"],
+ button_color=C["accent_dim"],
+ button_hover_color=C["accent"],
+ dropdown_fg_color=C["bg_card"],
+ corner_radius=8,
+ command=self._on_mic_change,
+ dynamic_resizing=False,
+ width=280,
+ )
+ self.mic_menu.pack(side="right", expand=True, fill="x", padx=(0, 0))
+
+ # ── Status Label ──────────────────────────────────────────────
+
def _build_status(self):
f = ctk.CTkFrame(self.main, fg_color="transparent")
- f.pack(fill="x", pady=(0, 8))
- hk = self.config.hotkey.upper()
- self.status = ctk.CTkLabel(f, text=f"✨ Gotowy — Przytrzymaj {hk} lub kliknij przycisk",
- font=ctk.CTkFont(size=12), text_color=C["txt2"])
+ f.pack(fill="x", pady=(4, 6))
+ hk = self.config.hotkey.upper().replace("+", " + ")
+ self.status = ctk.CTkLabel(
+ f,
+ text=f"✨ Gotowy — Przytrzymaj {hk} lub kliknij przycisk",
+ font=ctk.CTkFont(family="Segoe UI", size=12),
+ text_color=C["txt2"],
+ )
self.status.pack()
+ # ── Transcript Box ────────────────────────────────────────────
+
def _build_transcript(self):
- card = ctk.CTkFrame(self.main, fg_color=C["bg_card"], corner_radius=12,
- border_width=1, border_color=C["border"])
- card.pack(fill="both", expand=True, pady=(0, 8))
+ card = ctk.CTkFrame(
+ self.main, fg_color=C["bg_card"],
+ corner_radius=12,
+ border_width=1, border_color=C["border"],
+ )
+ card.pack(fill="both", expand=True, pady=(0, 6))
head = ctk.CTkFrame(card, fg_color="transparent")
head.pack(fill="x", padx=14, pady=(10, 0))
- ctk.CTkLabel(head, text="📝 Transkrypcja",
- font=ctk.CTkFont(size=13, weight="bold"),
- text_color=C["txt2"]).pack(side="left")
-
- self.copy_btn = ctk.CTkButton(head, text="📋 Kopiuj", width=75, height=26,
- font=ctk.CTkFont(size=11),
- fg_color=C["bg_hover"], hover_color=C["accent"],
- corner_radius=8, command=self._copy_text)
- self.copy_btn.pack(side="right")
-
- self.textbox = ctk.CTkTextbox(card, font=ctk.CTkFont(family="Segoe UI", size=15),
- fg_color="transparent", text_color=C["txt"],
- wrap="word", height=100, border_width=0)
- self.textbox.pack(fill="both", expand=True, padx=14, pady=(4, 10))
+ ctk.CTkLabel(
+ head, text="📝 Transkrypcja",
+ font=ctk.CTkFont(size=12, weight="bold"),
+ text_color=C["txt2"],
+ ).pack(side="left")
+
+ btn_row = ctk.CTkFrame(head, fg_color="transparent")
+ btn_row.pack(side="right")
+
+ ctk.CTkButton(
+ btn_row, text="🗑", width=30, height=26,
+ font=ctk.CTkFont(size=11),
+ fg_color=C["bg_hover"], hover_color=C["rec_red"],
+ corner_radius=8, command=self._clear_transcript,
+ ).pack(side="right", padx=(4, 0))
+
+ ctk.CTkButton(
+ btn_row, text="📋 Kopiuj", width=80, height=26,
+ font=ctk.CTkFont(size=11),
+ fg_color=C["bg_hover"], hover_color=C["accent"],
+ corner_radius=8, command=self._copy_text,
+ ).pack(side="right")
+
+ self.textbox = ctk.CTkTextbox(
+ card,
+ font=ctk.CTkFont(family="Segoe UI", size=14),
+ fg_color="transparent",
+ text_color=C["txt"],
+ wrap="word", height=90, border_width=0,
+ )
+ self.textbox.pack(fill="both", expand=True, padx=14, pady=(6, 10))
self.textbox.insert("1.0", "Twój tekst pojawi się tutaj...")
self.textbox.configure(state="disabled")
+ # ── Quick Controls (language / model / hotkey) ─────────────────
+
def _build_quick_controls(self):
- card = ctk.CTkFrame(self.main, fg_color=C["bg_card"], corner_radius=12,
- border_width=1, border_color=C["border"])
- card.pack(fill="x", pady=(0, 8))
+ card = ctk.CTkFrame(
+ self.main, fg_color=C["bg_card"],
+ corner_radius=12,
+ border_width=1, border_color=C["border"],
+ )
+ card.pack(fill="x", pady=(0, 6))
inner = ctk.CTkFrame(card, fg_color="transparent")
- inner.pack(fill="x", padx=14, pady=10)
+ inner.pack(fill="x", padx=12, pady=10)
# Language
lf = ctk.CTkFrame(inner, fg_color="transparent")
- lf.pack(side="left", expand=True, fill="x", padx=(0, 6))
- ctk.CTkLabel(lf, text="🌍 Język", font=ctk.CTkFont(size=11, weight="bold"),
- text_color=C["txt2"]).pack(anchor="w")
+ lf.pack(side="left", expand=True, fill="x", padx=(0, 5))
+ ctk.CTkLabel(lf, text="🌍 Język", font=ctk.CTkFont(size=10, weight="bold"),
+ text_color=C["txt3"]).pack(anchor="w")
self.lang_var = ctk.StringVar(value=self.config.language)
- ctk.CTkOptionMenu(lf, values=["auto", "pl", "en"], variable=self.lang_var,
- font=ctk.CTkFont(size=12), fg_color=C["bg_input"],
- button_color=C["accent"], button_hover_color=C["accent2"],
- dropdown_fg_color=C["bg_card"], corner_radius=8,
- command=self._on_lang).pack(fill="x", pady=(3, 0))
+ ctk.CTkOptionMenu(
+ lf, values=["auto", "pl", "en"],
+ variable=self.lang_var,
+ font=ctk.CTkFont(size=11),
+ fg_color=C["bg_input"],
+ button_color=C["accent_dim"],
+ button_hover_color=C["accent"],
+ dropdown_fg_color=C["bg_card"],
+ corner_radius=8,
+ command=self._on_lang,
+ ).pack(fill="x", pady=(3, 0))
# Model
mf = ctk.CTkFrame(inner, fg_color="transparent")
- mf.pack(side="left", expand=True, fill="x", padx=(6, 6))
- ctk.CTkLabel(mf, text="🧠 Model", font=ctk.CTkFont(size=11, weight="bold"),
- text_color=C["txt2"]).pack(anchor="w")
+ mf.pack(side="left", expand=True, fill="x", padx=(5, 5))
+ ctk.CTkLabel(mf, text="🧠 Model", font=ctk.CTkFont(size=10, weight="bold"),
+ text_color=C["txt3"]).pack(anchor="w")
self.model_var = ctk.StringVar(value=self.config.model_size)
- ctk.CTkOptionMenu(mf, values=self.config.available_models, variable=self.model_var,
- font=ctk.CTkFont(size=12), fg_color=C["bg_input"],
- button_color=C["accent"], button_hover_color=C["accent2"],
- dropdown_fg_color=C["bg_card"], corner_radius=8,
- command=self._on_model).pack(fill="x", pady=(3, 0))
-
- # Hotkey
+ ctk.CTkOptionMenu(
+ mf, values=self.config.available_models,
+ variable=self.model_var,
+ font=ctk.CTkFont(size=11),
+ fg_color=C["bg_input"],
+ button_color=C["accent_dim"],
+ button_hover_color=C["accent"],
+ dropdown_fg_color=C["bg_card"],
+ corner_radius=8,
+ command=self._on_model,
+ ).pack(fill="x", pady=(3, 0))
+
+ # Hotkey picker
hf = ctk.CTkFrame(inner, fg_color="transparent")
- hf.pack(side="left", expand=True, fill="x", padx=(6, 0))
- ctk.CTkLabel(hf, text="⌨️ Klawisz", font=ctk.CTkFont(size=11, weight="bold"),
- text_color=C["txt2"]).pack(anchor="w")
- self.hotkey_var = ctk.StringVar(value=self.config.hotkey)
- ctk.CTkOptionMenu(hf, values=self.config.available_hotkeys, variable=self.hotkey_var,
- font=ctk.CTkFont(size=12), fg_color=C["bg_input"],
- button_color=C["accent"], button_hover_color=C["accent2"],
- dropdown_fg_color=C["bg_card"], corner_radius=8,
- command=self._on_hotkey_change).pack(fill="x", pady=(3, 0))
+ hf.pack(side="left", expand=True, fill="x", padx=(5, 0))
+ ctk.CTkLabel(hf, text="⌨️ Klawisz", font=ctk.CTkFont(size=10, weight="bold"),
+ text_color=C["txt3"]).pack(anchor="w")
+ self.hotkey_btn = ctk.CTkButton(
+ hf,
+ text=self.config.hotkey.upper().replace("+", "+"),
+ height=32,
+ font=ctk.CTkFont(size=11, weight="bold"),
+ fg_color=C["bg_input"],
+ hover_color=C["accent_dim"],
+ border_width=1,
+ border_color=C["border2"],
+ corner_radius=8,
+ command=self._start_hotkey_capture,
+ )
+ self.hotkey_btn.pack(fill="x", pady=(3, 0))
+
+ # ── History ───────────────────────────────────────────────────
def _build_history(self):
- card = ctk.CTkFrame(self.main, fg_color=C["bg_card"], corner_radius=12,
- border_width=1, border_color=C["border"])
- card.pack(fill="x", pady=(0, 8))
+ card = ctk.CTkFrame(
+ self.main, fg_color=C["bg_card"],
+ corner_radius=12,
+ border_width=1, border_color=C["border"],
+ )
+ card.pack(fill="x", pady=(0, 6))
head = ctk.CTkFrame(card, fg_color="transparent")
head.pack(fill="x", padx=14, pady=(10, 4))
- ctk.CTkLabel(head, text="📚 Historia",
- font=ctk.CTkFont(size=13, weight="bold"),
- text_color=C["txt2"]).pack(side="left")
-
- ctk.CTkButton(head, text="🗑️", width=30, height=26,
- font=ctk.CTkFont(size=10), fg_color=C["bg_hover"],
- hover_color=C["rec_red"], corner_radius=6,
- command=self._clear_history).pack(side="right")
+ ctk.CTkLabel(
+ head, text="📚 Historia",
+ font=ctk.CTkFont(size=12, weight="bold"),
+ text_color=C["txt2"],
+ ).pack(side="left")
+
+ ctk.CTkButton(
+ head, text="🗑", width=30, height=26,
+ font=ctk.CTkFont(size=10),
+ fg_color=C["bg_hover"],
+ hover_color=C["rec_red"],
+ corner_radius=6,
+ command=self._clear_history,
+ ).pack(side="right")
self.hist_frame = ctk.CTkFrame(card, fg_color="transparent")
- self.hist_frame.pack(fill="x", padx=14, pady=(0, 10))
+ self.hist_frame.pack(fill="x", padx=12, pady=(0, 10))
- self.hist_empty = ctk.CTkLabel(self.hist_frame, text="Brak nagrań",
- font=ctk.CTkFont(size=11), text_color=C["txt3"])
- self.hist_empty.pack(pady=4)
+ ctk.CTkLabel(
+ self.hist_frame, text="Brak nagrań",
+ font=ctk.CTkFont(size=11), text_color=C["txt3"],
+ ).pack(pady=4)
+
+ # ── Settings Panel ────────────────────────────────────────────
def _build_settings_panel(self):
"""Build the collapsible advanced settings panel."""
- self.settings_frame = ctk.CTkFrame(self.main, fg_color=C["bg_card"], corner_radius=12,
- border_width=1, border_color=C["accent_dim"])
- # Hidden by default
+ self.settings_frame = ctk.CTkFrame(
+ self.main, fg_color=C["bg_card"],
+ corner_radius=12,
+ border_width=1, border_color=C["accent_dim"],
+ )
self._settings_visible = False
inner = ctk.CTkFrame(self.settings_frame, fg_color="transparent")
inner.pack(fill="x", padx=14, pady=12)
- ctk.CTkLabel(inner, text="⚙️ Ustawienia zaawansowane",
- font=ctk.CTkFont(size=14, weight="bold"),
- text_color=C["txt"]).pack(anchor="w", pady=(0, 10))
+ ctk.CTkLabel(
+ inner, text="⚙️ Ustawienia zaawansowane",
+ font=ctk.CTkFont(size=13, weight="bold"),
+ text_color=C["txt"],
+ ).pack(anchor="w", pady=(0, 10))
+
+ def sw_row(parent, label, var, cmd):
+ r = ctk.CTkFrame(parent, fg_color="transparent")
+ r.pack(fill="x", pady=3)
+ ctk.CTkLabel(r, text=label, font=ctk.CTkFont(size=12),
+ text_color=C["txt2"]).pack(side="left")
+ ctk.CTkSwitch(r, text="", variable=var,
+ fg_color=C["bg_hover"], progress_color=C["accent"],
+ button_color=C["txt"], button_hover_color=C["accent3"],
+ command=cmd).pack(side="right")
+
+ def opt_row(parent, label, values, var, cmd, width=130):
+ r = ctk.CTkFrame(parent, fg_color="transparent")
+ r.pack(fill="x", pady=3)
+ ctk.CTkLabel(r, text=label, font=ctk.CTkFont(size=12),
+ text_color=C["txt2"]).pack(side="left")
+ ctk.CTkOptionMenu(r, values=values, variable=var, width=width,
+ font=ctk.CTkFont(size=11),
+ fg_color=C["bg_input"], button_color=C["accent_dim"],
+ button_hover_color=C["accent"],
+ dropdown_fg_color=C["bg_card"],
+ corner_radius=8, command=cmd).pack(side="right")
- # --- Auto-type toggle ---
- row1 = ctk.CTkFrame(inner, fg_color="transparent")
- row1.pack(fill="x", pady=3)
- ctk.CTkLabel(row1, text="✍️ Auto-wpisywanie tekstu",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
self.autotype_var = ctk.BooleanVar(value=self.config.auto_type_enabled)
- ctk.CTkSwitch(row1, text="", variable=self.autotype_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_autotype_toggle).pack(side="right")
-
- # --- Typing method ---
- row2 = ctk.CTkFrame(inner, fg_color="transparent")
- row2.pack(fill="x", pady=3)
- ctk.CTkLabel(row2, text="📋 Metoda wpisywania",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ sw_row(inner, "✍️ Auto-wpisywanie tekstu", self.autotype_var, self._on_autotype_toggle)
+
self.typemethod_var = ctk.StringVar(value=self.config.typing_method)
- ctk.CTkOptionMenu(row2, values=["clipboard", "keyboard"], variable=self.typemethod_var,
- width=130, font=ctk.CTkFont(size=11),
- fg_color=C["bg_input"], button_color=C["accent"],
- button_hover_color=C["accent2"], dropdown_fg_color=C["bg_card"],
- corner_radius=8, command=self._on_typemethod).pack(side="right")
-
- # --- Auto-copy ---
- row3 = ctk.CTkFrame(inner, fg_color="transparent")
- row3.pack(fill="x", pady=3)
- ctk.CTkLabel(row3, text="📎 Auto-kopiowanie do schowka",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ opt_row(inner, "📋 Metoda wpisywania", ["clipboard", "keyboard"],
+ self.typemethod_var, self._on_typemethod)
+
self.autocopy_var = ctk.BooleanVar(value=self.config.auto_copy_to_clipboard)
- ctk.CTkSwitch(row3, text="", variable=self.autocopy_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_autocopy_toggle).pack(side="right")
-
- # --- Minimize to tray ---
- row4 = ctk.CTkFrame(inner, fg_color="transparent")
- row4.pack(fill="x", pady=3)
- ctk.CTkLabel(row4, text="🔲 Minimalizuj do zasobnika",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ sw_row(inner, "📎 Auto-kopiowanie do schowka", self.autocopy_var, self._on_autocopy_toggle)
+
self.tray_var = ctk.BooleanVar(value=self.config.minimize_to_tray)
- ctk.CTkSwitch(row4, text="", variable=self.tray_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_tray_toggle).pack(side="right")
-
- # --- Notifications ---
- row5 = ctk.CTkFrame(inner, fg_color="transparent")
- row5.pack(fill="x", pady=3)
- ctk.CTkLabel(row5, text="🔔 Powiadomienia",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ sw_row(inner, "🔲 Minimalizuj do zasobnika", self.tray_var, self._on_tray_toggle)
+
self.notify_var = ctk.BooleanVar(value=self.config.show_notifications)
- ctk.CTkSwitch(row5, text="", variable=self.notify_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_notify_toggle).pack(side="right")
-
- # --- VAD ---
- row6 = ctk.CTkFrame(inner, fg_color="transparent")
- row6.pack(fill="x", pady=3)
- ctk.CTkLabel(row6, text="🎯 Detekcja mowy (VAD)",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ sw_row(inner, "🔔 Powiadomienia", self.notify_var, self._on_notify_toggle)
+
self.vad_var = ctk.BooleanVar(value=self.config.vad_enabled)
- ctk.CTkSwitch(row6, text="", variable=self.vad_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_vad_toggle).pack(side="right")
-
- # --- Beam Size ---
- row7 = ctk.CTkFrame(inner, fg_color="transparent")
- row7.pack(fill="x", pady=3)
- ctk.CTkLabel(row7, text="🔬 Beam size (dokładność)",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ sw_row(inner, "🎯 Detekcja mowy (VAD)", self.vad_var, self._on_vad_toggle)
+
self.beam_var = ctk.StringVar(value=str(self.config.beam_size))
- ctk.CTkOptionMenu(row7, values=["1", "3", "5", "8", "10"], variable=self.beam_var,
- width=80, font=ctk.CTkFont(size=11),
- fg_color=C["bg_input"], button_color=C["accent"],
- button_hover_color=C["accent2"], dropdown_fg_color=C["bg_card"],
- corner_radius=8, command=self._on_beam_change).pack(side="right")
-
- # --- Auto-correct ---
- row8 = ctk.CTkFrame(inner, fg_color="transparent")
- row8.pack(fill="x", pady=3)
- ctk.CTkLabel(row8, text="✨ Autokorekta tekstu",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ opt_row(inner, "🔬 Beam size (dokładność)", ["1", "3", "5", "8", "10"],
+ self.beam_var, self._on_beam_change, width=80)
+
self.autocorrect_var = ctk.BooleanVar(value=self.config.auto_correct)
- ctk.CTkSwitch(row8, text="", variable=self.autocorrect_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_autocorrect_toggle).pack(side="right")
-
- # --- Sounds toggle ---
- row9 = ctk.CTkFrame(inner, fg_color="transparent")
- row9.pack(fill="x", pady=3)
- ctk.CTkLabel(row9, text="🔊 Dźwięki nagrywania",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
+ sw_row(inner, "✨ Autokorekta tekstu", self.autocorrect_var, self._on_autocorrect_toggle)
+
self.sounds_var = ctk.BooleanVar(value=self.config.play_sounds)
- ctk.CTkSwitch(row9, text="", variable=self.sounds_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_sounds_toggle).pack(side="right")
-
- # --- Auto-start with Windows ---
- row10 = ctk.CTkFrame(inner, fg_color="transparent")
- row10.pack(fill="x", pady=3)
- ctk.CTkLabel(row10, text="🚀 Uruchamiaj z Windows",
- font=ctk.CTkFont(size=12), text_color=C["txt2"]).pack(side="left")
- self.autostart_var = ctk.BooleanVar(value=is_autostart_enabled())
- ctk.CTkSwitch(row10, text="", variable=self.autostart_var,
- fg_color=C["bg_hover"], progress_color=C["accent"],
- button_color=C["txt"], button_hover_color=C["accent2"],
- command=self._on_autostart_toggle).pack(side="right")
+ sw_row(inner, "🔊 Dźwięki nagrywania", self.sounds_var, self._on_sounds_toggle)
- # --- Separator ---
- ctk.CTkFrame(inner, fg_color=C["border"], height=1).pack(fill="x", pady=8)
+ self.autostart_var = ctk.BooleanVar(value=is_autostart_enabled())
+ sw_row(inner, "🚀 Uruchamiaj z Windows", self.autostart_var, self._on_autostart_toggle)
- # Info label
- ctk.CTkLabel(inner,
- text="💡 Dźwięki informują o starcie/stopie nagrywania.\n"
- " Animacja REC pojawia się w prawym górnym rogu ekranu.",
- font=ctk.CTkFont(size=11), text_color=C["txt3"],
- justify="left").pack(anchor="w")
+ ctk.CTkFrame(inner, fg_color=C["border"], height=1).pack(fill="x", pady=(8, 6))
- def _build_footer(self):
- f = ctk.CTkFrame(self.main, fg_color="transparent")
- f.pack(fill="x", pady=(0, 6))
+ ctk.CTkLabel(
+ inner,
+ text="💡 Wskazówka: Użyj Ctrl+Space lub F2–F10 jako klawisz dyktowania.\n"
+ " Model 'small' = dobry balans jakości i szybkości.",
+ font=ctk.CTkFont(size=10),
+ text_color=C["txt3"],
+ justify="left",
+ ).pack(anchor="w")
- # Main footer with branding
- ctk.CTkLabel(f, text=f"VoxFlow v{__version__} • faster-whisper • 100% Lokalne",
- font=ctk.CTkFont(size=10), text_color=C["txt3"]).pack()
+ # ── Footer ────────────────────────────────────────────────────
- # AI Evolution Polska credit
- ctk.CTkLabel(f, text=f"Built by {__author__}",
- font=ctk.CTkFont(size=9, weight="bold"),
- text_color=C["accent_dim"]).pack(pady=(1, 0))
+ def _build_footer(self):
+ self.footer_frame = ctk.CTkFrame(self.main, fg_color="transparent")
+ self.footer_frame.pack(fill="x", pady=(0, 4))
+
+ ctk.CTkLabel(
+ self.footer_frame,
+ text=f"VoxFlow v{__version__} • faster-whisper • 100% lokalne",
+ font=ctk.CTkFont(size=9),
+ text_color=C["txt3"],
+ ).pack()
+
+ ctk.CTkLabel(
+ self.footer_frame,
+ text=f"Open Source • {__author__}",
+ font=ctk.CTkFont(size=9, weight="bold"),
+ text_color=C["accent_dim"],
+ ).pack(pady=(1, 0))
# ═══════════════════════════════════════════════════════════════
# SETTINGS TOGGLE
@@ -476,8 +679,10 @@ def _toggle_settings(self):
self._settings_visible = False
self.settings_btn.configure(fg_color=C["bg_card"])
else:
- # Pack before footer
- self.settings_frame.pack(fill="x", pady=(0, 8), before=self.main.winfo_children()[-1])
+ self.settings_frame.pack(
+ fill="x", pady=(0, 6),
+ before=self.footer_frame, # Always insert just before footer
+ )
self._settings_visible = True
self.settings_btn.configure(fg_color=C["accent_dim"])
@@ -488,26 +693,148 @@ def _toggle_settings(self):
def _animate(self):
if not self._alive:
return
- self._phase += 0.12
+ self._phase += 0.10
self._draw_btn()
target = self._level if self._recording else 0
cur = self.level_bar.get()
- self.level_bar.set(min(1.0, cur + (target - cur) * 0.3) * 5 if self._recording else max(0, cur * 0.85))
- self.level_bar.configure(progress_color=C["rec_red"] if self._recording else C["accent"])
+ if self._recording:
+ self.level_bar.set(min(1.0, cur + (target - cur) * 0.3) * 5)
+ else:
+ self.level_bar.set(max(0.0, cur * 0.8))
+ self.level_bar.configure(
+ progress_color=C["rec_red"] if self._recording else C["accent"]
+ )
self.after(50, self._animate)
+ # ═══════════════════════════════════════════════════════════════
+ # HOTKEY CAPTURE (interactive)
+ # ═══════════════════════════════════════════════════════════════
+
+ def _start_hotkey_capture(self):
+ """Start interactive hotkey capture — next key press becomes the hotkey."""
+ if self._capturing_hotkey or self._recording:
+ return
+ self._capturing_hotkey = True
+ self.hotkey_btn.configure(
+ text="⌨️ Naciśnij klawisz...",
+ fg_color=C["accent_dim"],
+ border_color=C["accent"],
+ )
+ self.hotkey_manager.stop()
+ threading.Thread(target=self._capture_hotkey_thread, daemon=True).start()
+
+ def _capture_hotkey_thread(self):
+ """Wait for key press in background thread."""
+ key = self.hotkey_manager.capture_next_key(timeout=10.0)
+ self.after(0, lambda: self._finish_hotkey_capture(key))
+
+ def _finish_hotkey_capture(self, key: Optional[str]):
+ """Apply captured key as new hotkey."""
+ self._capturing_hotkey = False
+ if key:
+ self.config.hotkey = key
+ self.config.save()
+ self.hotkey_manager.update_hotkey(key)
+ display = key.upper().replace("+", " + ")
+ self.hotkey_btn.configure(
+ text=display,
+ fg_color=C["bg_input"],
+ border_color=C["ok"],
+ )
+ self.subtitle_label.configure(
+ text=f"Przytrzymaj {display} aby dyktować • 100% offline"
+ )
+ hk_disp = key.upper().replace("+", " + ")
+ self.status.configure(
+ text=f"⌨️ Hotkey ustawiony: {hk_disp}", text_color=C["ok"]
+ )
+ # Reset border after 2s
+ self.after(2000, lambda: self.hotkey_btn.configure(border_color=C["border2"]))
+ else:
+ # Cancelled / timeout
+ self.hotkey_btn.configure(
+ text=self.config.hotkey.upper().replace("+", " + "),
+ fg_color=C["bg_input"],
+ border_color=C["border2"],
+ )
+ self.hotkey_manager.start()
+
+ # ═══════════════════════════════════════════════════════════════
+ # MICROPHONE SELECTION
+ # ═══════════════════════════════════════════════════════════════
+
+ def _on_mic_change(self, label: str):
+ """Handle microphone device selection change."""
+ if label.startswith("🎤 Domyślny"):
+ idx = -1
+ else:
+ try:
+ idx = int(label.split("]")[0].replace("[", "").strip())
+ except (ValueError, IndexError):
+ idx = -1
+
+ self.config.audio_device_index = idx
+ self.config.save()
+ self.recorder.set_device(idx)
+
+ dev_name = label if idx < 0 else label[label.index("]") + 2:]
+ self.status.configure(
+ text=f"🎙 Mikrofon: {dev_name[:40]}", text_color=C["ok"]
+ )
+
+ def _on_device_fallback(self, fallback_index: int):
+ """Called by AudioRecorder when it falls back to the default device."""
+ self.config.audio_device_index = fallback_index
+ self.config.save()
+ # Update mic dropdown to show "Domyślny mikrofon"
+ self.after(0, lambda: (
+ self.mic_var.set("🎤 Domyślny mikrofon")
+ if hasattr(self, "mic_var") else None
+ ))
+ self.after(
+ 0,
+ lambda: self.status.configure(
+ text="⚠️ Mikrofon niedostępny, przełączono na domyślny",
+ text_color=C["warn"],
+ ),
+ )
+
+ def _on_window_configure(self, event=None):
+ """Save window dimensions whenever the window is resized.
+
+ Debounced: cancels any pending save and schedules a new one
+ 400 ms after the last resize event.
+ """
+ if hasattr(self, "_resize_job") and self._resize_job:
+ try:
+ self.after_cancel(self._resize_job)
+ except Exception:
+ pass
+ self._resize_job = self.after(400, self._save_window_size)
+
+ def _save_window_size(self):
+ """Persist the current window dimensions to config."""
+ try:
+ w = self.winfo_width()
+ h = self.winfo_height()
+ if w > 100 and h > 100: # sanity check
+ self.config.window_width = w
+ self.config.window_height = h
+ self.config.save()
+ except Exception:
+ pass
+ self._resize_job = None
+
# ═══════════════════════════════════════════════════════════════
# RECORDING (Hold-to-Record)
# ═══════════════════════════════════════════════════════════════
def _on_hotkey_press(self):
- """Called when hotkey is pressed down — start recording."""
self.after(0, self._start_rec)
def _on_hotkey_release(self):
- """Called when hotkey is released — stop recording + transcribe."""
self.after(0, self._stop_rec)
def _toggle_recording(self):
@@ -519,11 +846,12 @@ def _toggle_recording(self):
self._start_rec()
def _start_rec(self):
- if self._recording or self._processing:
+ if self._recording or self._processing or self._capturing_hotkey:
return
self._recording = True
self.status.configure(text="🔴 Nagrywam... Mów teraz!", text_color=C["rec_red"])
- self.tray.set_recording(True)
+ if self.tray:
+ self.tray.set_recording(True)
if self.config.play_sounds:
sounds.play("start")
self.overlay.show(self)
@@ -534,7 +862,8 @@ def _stop_rec(self):
return
self._recording = False
self._processing = True
- self.tray.set_recording(False)
+ if self.tray:
+ self.tray.set_recording(False)
self.overlay.hide()
if self.config.play_sounds:
sounds.play("stop")
@@ -556,7 +885,9 @@ def _transcribe(self, audio: np.ndarray):
beam_size=self.config.beam_size,
vad_enabled=self.config.vad_enabled,
auto_correct=self.config.auto_correct,
- on_progress=lambda m: self.after(0, lambda msg=m: self.status.configure(text=msg)),
+ on_progress=lambda m: self.after(
+ 0, lambda msg=m: self.status.configure(text=msg)
+ ),
)
self.after(0, lambda: self._on_done(result))
except Exception as e:
@@ -584,7 +915,6 @@ def _on_done(self, result: dict):
# Auto-type into active window
if self.config.auto_type_enabled:
- # Small delay to let the user's target window regain focus
threading.Thread(
target=self._delayed_auto_type,
args=(text,),
@@ -594,16 +924,22 @@ def _on_done(self, result: dict):
lang = result.get("language", "?")
flag = "🇵🇱" if lang == "pl" else "🇬🇧" if lang == "en" else "🌍"
dur = result.get("duration", 0)
- extra = " • ✍️ Wpisano!" if self.config.auto_type_enabled else ""
- extra += " • 📋" if self.config.auto_copy_to_clipboard else ""
- self.status.configure(text=f"✅ {flag} {lang.upper()} • {dur:.1f}s{extra}", text_color=C["ok"])
+ extras = []
+ if self.config.auto_type_enabled:
+ extras.append("✍️")
+ if self.config.auto_copy_to_clipboard:
+ extras.append("📋")
+ extra_str = " • " + " ".join(extras) if extras else ""
+ self.status.configure(
+ text=f"✅ {flag} {lang.upper()} • {dur:.1f}s{extra_str}",
+ text_color=C["ok"],
+ )
if self.config.play_sounds:
sounds.play("done")
self._add_history(text, lang, dur)
def _delayed_auto_type(self, text: str):
- """Wait briefly for target window to get focus, then type."""
time.sleep(0.15)
self.auto_typer.type_text(text, method=self.config.typing_method)
@@ -618,32 +954,48 @@ def _on_error(self, err: str):
# ═══════════════════════════════════════════════════════════════
def _add_history(self, text: str, lang: str, dur: float):
- self._history.insert(0, {"text": text, "language": lang, "duration": dur,
- "time": datetime.now().strftime("%H:%M:%S")})
- if len(self._history) > 10:
- self._history = self._history[:10]
+ self._history.insert(0, {
+ "text": text, "language": lang,
+ "duration": dur,
+ "time": datetime.now().strftime("%H:%M"),
+ })
+ if len(self._history) > 20:
+ self._history = self._history[:20]
self._refresh_history()
def _refresh_history(self):
for w in self.hist_frame.winfo_children():
w.destroy()
if not self._history:
- ctk.CTkLabel(self.hist_frame, text="Brak nagrań",
- font=ctk.CTkFont(size=11), text_color=C["txt3"]).pack(pady=4)
+ ctk.CTkLabel(
+ self.hist_frame, text="Brak nagrań",
+ font=ctk.CTkFont(size=11), text_color=C["txt3"],
+ ).pack(pady=4)
return
- for e in self._history[:5]:
- row = ctk.CTkFrame(self.hist_frame, fg_color=C["bg_hover"], corner_radius=8, height=34)
+ for e in self._history[:6]:
+ row = ctk.CTkFrame(
+ self.hist_frame, fg_color=C["bg_hover"],
+ corner_radius=8, height=32,
+ )
row.pack(fill="x", pady=2)
row.pack_propagate(False)
flag = "🇵🇱" if e["language"] == "pl" else "🇬🇧" if e["language"] == "en" else "🌍"
- preview = e["text"][:45] + ("..." if len(e["text"]) > 45 else "")
- ctk.CTkLabel(row, text=f"{flag} {e['time']} • {preview}",
- font=ctk.CTkFont(size=11), text_color=C["txt2"],
- anchor="w").pack(side="left", padx=8, fill="x", expand=True)
+ preview = e["text"][:44] + ("…" if len(e["text"]) > 44 else "")
+ ctk.CTkLabel(
+ row,
+ text=f"{flag} {e['time']} • {preview}",
+ font=ctk.CTkFont(family="Segoe UI", size=10),
+ text_color=C["txt2"],
+ anchor="w",
+ ).pack(side="left", padx=8, fill="x", expand=True)
t = e["text"]
- ctk.CTkButton(row, text="📋", width=28, height=22, font=ctk.CTkFont(size=10),
- fg_color="transparent", hover_color=C["accent"], corner_radius=6,
- command=lambda txt=t: pyperclip.copy(txt)).pack(side="right", padx=4)
+ ctk.CTkButton(
+ row, text="📋", width=26, height=22,
+ font=ctk.CTkFont(size=10),
+ fg_color="transparent", hover_color=C["accent"],
+ corner_radius=6,
+ command=lambda txt=t: pyperclip.copy(txt),
+ ).pack(side="right", padx=4)
def _clear_history(self):
self._history = []
@@ -654,61 +1006,75 @@ def _clear_history(self):
# ═══════════════════════════════════════════════════════════════
def _on_lang(self, v):
- self.config.language = v; self.config.save()
+ self.config.language = v
+ self.config.save()
def _on_model(self, v):
if v != self.config.model_size:
- self.config.model_size = v; self.config.save()
+ self.config.model_size = v
+ self.config.save()
self.status.configure(text=f"⏳ Ładuję model '{v}'...", text_color=C["warn"])
threading.Thread(target=self._reload_model, args=(v,), daemon=True).start()
- def _on_hotkey_change(self, v):
- self.config.hotkey = v; self.config.save()
- self.hotkey_manager.update_hotkey(v)
- self.status.configure(text=f"⌨️ Hotkey: {v.upper()}", text_color=C["ok"])
-
def _on_autotype_toggle(self):
- self.config.auto_type_enabled = self.autotype_var.get(); self.config.save()
+ self.config.auto_type_enabled = self.autotype_var.get()
+ self.config.save()
def _on_typemethod(self, v):
- self.config.typing_method = v; self.config.save()
+ self.config.typing_method = v
+ self.config.save()
def _on_autocopy_toggle(self):
- self.config.auto_copy_to_clipboard = self.autocopy_var.get(); self.config.save()
+ self.config.auto_copy_to_clipboard = self.autocopy_var.get()
+ self.config.save()
def _on_tray_toggle(self):
- self.config.minimize_to_tray = self.tray_var.get(); self.config.save()
+ self.config.minimize_to_tray = self.tray_var.get()
+ self.config.save()
def _on_notify_toggle(self):
- self.config.show_notifications = self.notify_var.get(); self.config.save()
+ self.config.show_notifications = self.notify_var.get()
+ self.config.save()
def _on_vad_toggle(self):
- self.config.vad_enabled = self.vad_var.get(); self.config.save()
+ self.config.vad_enabled = self.vad_var.get()
+ self.config.save()
def _on_beam_change(self, v):
- self.config.beam_size = int(v); self.config.save()
+ self.config.beam_size = int(v)
+ self.config.save()
def _on_autocorrect_toggle(self):
- self.config.auto_correct = self.autocorrect_var.get(); self.config.save()
+ self.config.auto_correct = self.autocorrect_var.get()
+ self.config.save()
def _on_sounds_toggle(self):
- self.config.play_sounds = self.sounds_var.get(); self.config.save()
+ self.config.play_sounds = self.sounds_var.get()
+ self.config.save()
def _on_autostart_toggle(self):
enabled = self.autostart_var.get()
set_autostart(enabled)
self.config.start_with_windows = enabled
self.config.save()
- msg = "🚀 Auto-start włączony" if enabled else "🚀 Auto-start wyłączony"
+ msg = "🚀 Auto-start włączony" if enabled else "🔕 Auto-start wyłączony"
self.status.configure(text=msg, text_color=C["ok"])
def _reload_model(self, sz):
try:
- self.transcriber.load_model(sz, on_progress=lambda m: self.after(
- 0, lambda msg=m: self.status.configure(text=msg)))
+ self.transcriber.load_model(
+ sz,
+ on_progress=lambda m: self.after(
+ 0, lambda msg=m: self.status.configure(text=msg)
+ ),
+ )
except Exception as e:
- self.after(0, lambda: self.status.configure(
- text=f"❌ Model: {str(e)[:50]}", text_color=C["rec_red"]))
+ self.after(
+ 0,
+ lambda: self.status.configure(
+ text=f"❌ Model: {str(e)[:50]}", text_color=C["rec_red"]
+ ),
+ )
def _copy_text(self):
self.textbox.configure(state="normal")
@@ -718,6 +1084,12 @@ def _copy_text(self):
pyperclip.copy(t)
self.status.configure(text="📋 Skopiowano!", text_color=C["ok"])
+ def _clear_transcript(self):
+ self.textbox.configure(state="normal")
+ self.textbox.delete("1.0", "end")
+ self.textbox.insert("1.0", "Twój tekst pojawi się tutaj...")
+ self.textbox.configure(state="disabled")
+
def _on_level(self, lv):
self._level = lv
self.overlay.set_level(lv)
@@ -727,7 +1099,9 @@ def _on_level(self, lv):
# ═══════════════════════════════════════════════════════════════
def _start_services(self):
- self.status.configure(text="⏳ Ładowanie modelu Whisper...", text_color=C["warn"])
+ self.status.configure(
+ text="⏳ Ładowanie modelu Whisper...", text_color=C["warn"]
+ )
threading.Thread(target=self._init_model, daemon=True).start()
try:
self.hotkey_manager.start()
@@ -740,14 +1114,28 @@ def _start_services(self):
def _init_model(self):
try:
- self.transcriber.load_model(on_progress=lambda m: self.after(
- 0, lambda msg=m: self.status.configure(text=msg, text_color=C["txt2"])))
- hk = self.config.hotkey.upper()
- self.after(0, lambda: self.status.configure(
- text=f"✨ Gotowy — Przytrzymaj {hk} i mów", text_color=C["ok"]))
+ self.transcriber.load_model(
+ on_progress=lambda m: self.after(
+ 0, lambda msg=m: self.status.configure(
+ text=msg, text_color=C["txt2"]
+ )
+ )
+ )
+ hk = self.config.hotkey.upper().replace("+", " + ")
+ self.after(
+ 0,
+ lambda: self.status.configure(
+ text=f"✨ Gotowy — Przytrzymaj {hk} i mów",
+ text_color=C["ok"],
+ ),
+ )
except Exception as e:
- self.after(0, lambda: self.status.configure(
- text=f"❌ {str(e)[:70]}", text_color=C["rec_red"]))
+ self.after(
+ 0,
+ lambda: self.status.configure(
+ text=f"❌ {str(e)[:70]}", text_color=C["rec_red"]
+ ),
+ )
def _show(self):
self.after(0, lambda: (self.deiconify(), self.lift(), self.focus_force()))
diff --git a/voxflow/config.py b/voxflow/config.py
index 87703df..da626a3 100644
--- a/voxflow/config.py
+++ b/voxflow/config.py
@@ -19,7 +19,8 @@ def get_config_dir() -> Path:
# Valid value ranges for security validation
_VALID_MODELS = {"tiny", "base", "small", "medium", "large-v3"}
_VALID_LANGUAGES = {"auto", "pl", "en"}
-_VALID_HOTKEYS = {"f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10"}
+# Hotkey validation: allow any non-empty string (hotkey picker can produce
+# arbitrary key names). We only enforce it's a safe non-empty string.
_VALID_TYPING_METHODS = {"clipboard", "keyboard"}
_VALID_THEMES = {"dark", "light"}
@@ -60,8 +61,10 @@ def _validate_config(data: dict) -> dict:
validated[key] = default_val
elif key == "language" and value not in _VALID_LANGUAGES:
validated[key] = default_val
- elif key == "hotkey" and str(value).lower() not in _VALID_HOTKEYS:
- validated[key] = default_val
+ elif key == "hotkey":
+ # Accept any non-empty string — hotkey picker sets arbitrary keys
+ s = str(value).strip().lower()
+ validated[key] = s if s else default_val
elif key == "typing_method" and value not in _VALID_TYPING_METHODS:
validated[key] = default_val
elif key == "theme" and value not in _VALID_THEMES:
@@ -82,6 +85,8 @@ def _validate_config(data: dict) -> dict:
validated[key] = max(500, min(1080, int(value)))
elif key == "vad_silence_ms":
validated[key] = max(50, min(5000, int(value)))
+ elif key == "audio_device_index":
+ validated[key] = int(value) # -1 = default device
else:
validated[key] = value
@@ -104,6 +109,9 @@ class VoxFlowConfig:
silence_duration: float = 2.0
max_recording_duration: float = 300.0 # 5 min max
+ # Audio device (-1 = system default)
+ audio_device_index: int = -1
+
# Hotkey - hold-to-record
hotkey: str = "f2"
@@ -120,7 +128,7 @@ class VoxFlowConfig:
play_sounds: bool = True
theme: str = "dark"
window_width: int = 500
- window_height: int = 750
+ window_height: int = 780
# Advanced
beam_size: int = 5
@@ -166,7 +174,11 @@ def available_languages(self) -> dict:
@property
def available_hotkeys(self) -> list:
- return ["f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10"]
+ return [
+ "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10",
+ "ctrl+space", "ctrl+shift+space", "caps lock", "insert",
+ "scroll lock",
+ ]
@property
def available_typing_methods(self) -> dict:
diff --git a/voxflow/hotkey_manager.py b/voxflow/hotkey_manager.py
index 94667ca..f7085aa 100644
--- a/voxflow/hotkey_manager.py
+++ b/voxflow/hotkey_manager.py
@@ -1,17 +1,31 @@
-"""VoxFlow Hotkey Manager - Hold-to-record with F2 key.
+"""VoxFlow Hotkey Manager - Hold-to-record with configurable key.
-Press and HOLD F2 → recording starts
-Release F2 → recording stops → auto-transcribe → auto-type
+Press and HOLD key → recording starts
+Release key → recording stops → auto-transcribe → auto-type
+
+Supports:
+- Single function keys: f2, f3, ..., f10
+- Combo keys: ctrl+space, ctrl+shift+space
+- Special keys: caps lock, insert, scroll lock
"""
import threading
from typing import Optional, Callable
+# Keys that should NOT be accepted as standalone hotkeys (only as modifiers)
+_MODIFIER_KEYS = frozenset({
+ "shift", "ctrl", "alt", "windows", "win",
+ "left shift", "right shift",
+ "left ctrl", "right ctrl",
+ "left alt", "right alt",
+ "left windows", "right windows",
+})
+
class HotkeyManager:
"""Manages hold-to-record hotkey using keyboard hook.
- This uses keyboard.hook_key() which properly detects
- both key-down and key-up events for a single key.
+ Uses keyboard.hook_key() or keyboard.add_hotkey() depending on
+ whether the hotkey is a single key or a combination.
"""
def __init__(
@@ -26,6 +40,7 @@ def __init__(
self._active = False
self._is_held = False
self._hook_ref = None
+ self._is_combo = "+" in hotkey
def start(self):
"""Start listening for the hold-to-record hotkey."""
@@ -34,12 +49,17 @@ def start(self):
try:
import keyboard
- # hook_key fires for BOTH down and up events on this key
- self._hook_ref = keyboard.hook_key(
- self.hotkey,
- callback=self._on_key_event,
- suppress=False, # Don't suppress — works without admin
- )
+
+ if self._is_combo:
+ # Combo hotkeys: use suppress=True press/release hooks
+ self._hook_ref = keyboard.hook(self._on_key_event_combo)
+ else:
+ # Single key: hook_key fires for BOTH down and up events
+ self._hook_ref = keyboard.hook_key(
+ self.hotkey,
+ callback=self._on_key_event,
+ suppress=False,
+ )
self._active = True
print(f"Hotkey '{self.hotkey.upper()}' registered (hold-to-record)")
except Exception as e:
@@ -65,15 +85,50 @@ def update_hotkey(self, new_hotkey: str):
if was_active:
self.stop()
self.hotkey = new_hotkey.lower()
+ self._is_combo = "+" in new_hotkey
if was_active:
self.start()
+ def capture_next_key(self, timeout: float = 8.0) -> Optional[str]:
+ """Wait for the next key press and return its name.
+
+ Runs in a background thread with a strict timeout so it can't
+ hang the UI. Press Escape to cancel.
+
+ Returns:
+ Key name string, or None if timeout/cancelled/error.
+ """
+ result: list[Optional[str]] = [None]
+ done = threading.Event()
+
+ def _read():
+ try:
+ import keyboard
+ while not done.is_set():
+ event = keyboard.read_event(suppress=False)
+ if event and event.event_type == "down":
+ name = event.name.lower()
+ if name == "escape":
+ done.set() # cancelled
+ return
+ if name not in _MODIFIER_KEYS:
+ result[0] = name
+ done.set()
+ return
+ except Exception as e:
+ print(f"capture_next_key error: {e}")
+ done.set()
+
+ t = threading.Thread(target=_read, daemon=True)
+ t.start()
+ done.wait(timeout=timeout)
+ return result[0]
+
def _on_key_event(self, event):
- """Handle key events — detect press (down) and release (up)."""
+ """Handle single key events — detect press (down) and release (up)."""
try:
if event.event_type == "down":
if not self._is_held:
- # First press (ignore key repeats)
self._is_held = True
if self.on_press:
threading.Thread(target=self.on_press, daemon=True).start()
@@ -85,6 +140,28 @@ def _on_key_event(self, event):
except Exception as e:
print(f"Hotkey event error: {e}")
+ def _on_key_event_combo(self, event):
+ """Handle combo hotkey events via global keyboard hook."""
+ try:
+ import keyboard
+ # Check if currently held keys match the combo
+ keys = [k.strip() for k in self.hotkey.split("+")]
+ all_pressed = all(keyboard.is_pressed(k) for k in keys)
+
+ if event.event_type == "down" and all_pressed:
+ if not self._is_held:
+ self._is_held = True
+ if self.on_press:
+ threading.Thread(target=self.on_press, daemon=True).start()
+ elif event.event_type == "up" and self._is_held:
+ main_key = keys[-1]
+ if event.name.lower() == main_key or not all_pressed:
+ self._is_held = False
+ if self.on_release:
+ threading.Thread(target=self.on_release, daemon=True).start()
+ except Exception as e:
+ print(f"Combo hotkey event error: {e}")
+
@property
def is_active(self) -> bool:
return self._active
@@ -95,7 +172,9 @@ def is_held(self) -> bool:
@staticmethod
def get_available_hotkeys() -> list[str]:
- """Return list of recommended single-key hotkeys for hold-to-record."""
+ """Return list of recommended hotkeys for hold-to-record."""
return [
"f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10",
+ "ctrl+space", "ctrl+shift+space", "caps lock", "insert",
+ "scroll lock",
]
diff --git a/voxflow/main.py b/voxflow/main.py
index ad4f8dd..389d69d 100644
--- a/voxflow/main.py
+++ b/voxflow/main.py
@@ -10,6 +10,13 @@ def main():
# Set environment for better compatibility
os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
+ # Fix encoding for Windows consoles
+ if sys.platform == "win32":
+ if sys.stdout is not None:
+ sys.stdout.reconfigure(encoding='utf-8')
+ if sys.stderr is not None:
+ sys.stderr.reconfigure(encoding='utf-8')
+
# Handle --test mode
if "--test" in sys.argv:
print("🧪 VoxFlow Test Mode")
diff --git a/voxflow/overlay.py b/voxflow/overlay.py
index 3dd4743..179f7e7 100644
--- a/voxflow/overlay.py
+++ b/voxflow/overlay.py
@@ -1,24 +1,31 @@
"""VoxFlow Recording Overlay — On-screen animation while recording.
-Shows a small floating, always-on-top indicator with a pulsing red dot
-and audio waveform animation. The overlay is transparent and click-through
+Shows a sleek floating bar at the BOTTOM-CENTER of the screen with
+an animated audio waveform. The overlay is transparent and click-through
so it doesn't interfere with the user's work.
Built by AI Evolution Polska
"""
import threading
import math
-import time
import tkinter as tk
from typing import Optional
class RecordingOverlay:
- """Floating overlay that shows recording animation on screen."""
+ """Floating overlay that shows recording waveform animation at bottom of screen."""
- WIDTH = 200
- HEIGHT = 60
- CORNER_MARGIN = 20
+ WIDTH = 560
+ HEIGHT = 72
+ BOTTOM_MARGIN = 80 # pixels from bottom of screen
+
+ # Colours
+ BG = "#0d0d1f"
+ BORDER = "#7c3aed"
+ RED = "#ef4444"
+ PURPLE = "#a78bfa"
+ PURPLE2 = "#7c3aed"
+ GREY = "#94a3b8"
def __init__(self):
self._root: Optional[tk.Toplevel] = None
@@ -26,9 +33,10 @@ def __init__(self):
self._alive = False
self._phase = 0.0
self._level = 0.0
- self._thread: Optional[threading.Thread] = None
self._parent = None
+ # ── Public API ────────────────────────────────────────────────
+
def show(self, parent=None):
"""Show the recording overlay on screen."""
if self._alive:
@@ -38,8 +46,7 @@ def show(self, parent=None):
if parent:
parent.after(0, self._create_window)
else:
- self._thread = threading.Thread(target=self._run_standalone, daemon=True)
- self._thread.start()
+ threading.Thread(target=self._run_standalone, daemon=True).start()
def hide(self):
"""Hide the recording overlay."""
@@ -59,7 +66,9 @@ def hide(self):
def set_level(self, level: float):
"""Update the audio level (0.0 - 1.0)."""
- self._level = min(1.0, level * 5)
+ self._level = min(1.0, max(0.0, level))
+
+ # ── Window management ─────────────────────────────────────────
def _create_window(self):
"""Create the overlay window (must be called from main thread)."""
@@ -70,22 +79,22 @@ def _create_window(self):
self._root = tk.Tk()
win = self._root
- win.overrideredirect(True) # No window decorations
- win.attributes("-topmost", True) # Always on top
- win.attributes("-alpha", 0.9) # Slightly transparent
+ win.overrideredirect(True) # No title bar / borders
+ win.attributes("-topmost", True) # Always on top
+ win.attributes("-alpha", 0.93) # Slightly transparent
- # Position: top-right corner of screen
+ # Position: bottom-center of screen
screen_w = win.winfo_screenwidth()
- x = screen_w - self.WIDTH - self.CORNER_MARGIN
- y = self.CORNER_MARGIN
+ screen_h = win.winfo_screenheight()
+ x = (screen_w - self.WIDTH) // 2
+ y = screen_h - self.HEIGHT - self.BOTTOM_MARGIN
win.geometry(f"{self.WIDTH}x{self.HEIGHT}+{x}+{y}")
- # Dark background with rounded appearance
- win.configure(bg="#1a1a2e")
-
+ win.configure(bg=self.BG)
+
self._canvas = tk.Canvas(
win, width=self.WIDTH, height=self.HEIGHT,
- bg="#1a1a2e", highlightthickness=0,
+ bg=self.BG, highlightthickness=0,
)
self._canvas.pack(fill="both", expand=True)
@@ -94,7 +103,6 @@ def _create_window(self):
print(f"Overlay error: {e}")
def _destroy_window(self):
- """Destroy the overlay window."""
try:
if self._root:
self._root.destroy()
@@ -104,88 +112,110 @@ def _destroy_window(self):
self._canvas = None
def _run_standalone(self):
- """Run overlay in its own event loop (fallback)."""
self._create_window()
if self._root:
self._root.mainloop()
+ # ── Animation ─────────────────────────────────────────────────
+
def _animate(self):
- """Animation loop — pulsing dot + waveform bars."""
if not self._alive or not self._canvas:
return
- self._phase += 0.15
+ self._phase += 0.12
c = self._canvas
+ W, H = self.WIDTH, self.HEIGHT
c.delete("all")
- # Background rounded rect
- self._round_rect(c, 2, 2, self.WIDTH - 2, self.HEIGHT - 2, 12,
- fill="#1a1a2e", outline="#ef4444", width=2)
-
- # Pulsing red dot
- pulse = (math.sin(self._phase * 2) + 1) / 2
- dot_r = 6 + pulse * 3
- dot_x, dot_y = 22, self.HEIGHT // 2
- # Glow
- glow_r = dot_r + 4 + pulse * 4
- c.create_oval(dot_x - glow_r, dot_y - glow_r,
- dot_x + glow_r, dot_y + glow_r,
- fill="#ef444430", outline="")
- # Dot
- c.create_oval(dot_x - dot_r, dot_y - dot_r,
- dot_x + dot_r, dot_y + dot_r,
- fill="#ef4444", outline="#dc2626", width=1)
-
- # "REC" text
- c.create_text(48, dot_y - 8, text="● REC", fill="#ef4444",
- font=("Segoe UI", 11, "bold"), anchor="w")
- # "VoxFlow" small
- c.create_text(48, dot_y + 10, text="VoxFlow • AIEP", fill="#94a3b8",
- font=("Segoe UI", 8), anchor="w")
-
- # Waveform bars (right side)
- bar_start = 115
- num_bars = 10
- bar_width = 4
- bar_gap = 3
- max_h = 20
+ # ── Rounded background panel ──────────────────────────────
+ self._round_rect(c, 2, 2, W - 2, H - 2, r=16,
+ fill=self.BG, outline=self.BORDER, width=2)
+
+ # ── Pulsing mic icon (left side) ──────────────────────────
+ pulse = (math.sin(self._phase * 2.5) + 1) / 2
+ cx, cy = 36, H // 2
+
+ # Outer glow ring
+ glow_r = 18 + pulse * 5
+ c.create_oval(cx - glow_r, cy - glow_r,
+ cx + glow_r, cy + glow_r,
+ fill="", outline=self.RED, width=1)
+
+ # Inner filled dot
+ dot_r = 12 + pulse * 3
+ c.create_oval(cx - dot_r, cy - dot_r,
+ cx + dot_r, cy + dot_r,
+ fill=self.RED, outline="")
+
+ # Mic body (white)
+ c.create_oval(cx - 5, cy - 10, cx + 5, cy + 1,
+ fill="white", outline="")
+ # Mic stand
+ c.create_line(cx, cy + 1, cx, cy + 8, fill="white", width=2)
+ c.create_line(cx - 6, cy + 8, cx + 6, cy + 8, fill="white", width=2)
+
+ # ── Label text ────────────────────────────────────────────
+ c.create_text(64, cy - 10, text="🎤 Nagrywam...",
+ fill=self.RED, font=("Segoe UI", 11, "bold"), anchor="w")
+ c.create_text(64, cy + 9, text="VoxFlow • zwolnij klawisz aby zakończyć",
+ fill=self.GREY, font=("Segoe UI", 8), anchor="w")
+
+ # ── Waveform bars (right section) ─────────────────────────
+ num_bars = 28
+ bar_w = 4
+ bar_gap = 2
+ bar_section_x = 240
+ max_h = 22
for i in range(num_bars):
- # Mix of audio level and animation
- wave = abs(math.sin(self._phase * 2.5 + i * 0.6))
- amp = (self._level * 0.7 + wave * 0.3) * max_h + 2
- x = bar_start + i * (bar_width + bar_gap)
- cy = self.HEIGHT // 2
-
- # Color gradient from purple to red based on amplitude
- if amp > max_h * 0.7:
- color = "#ef4444"
- elif amp > max_h * 0.4:
- color = "#a78bfa"
+ # Combine audio level with animated sine waves
+ t = self._phase * 2.8 + i * 0.42
+ wave1 = abs(math.sin(t))
+ wave2 = abs(math.sin(t * 1.37 + 1.2)) * 0.5
+ combined = wave1 * 0.55 + wave2 * 0.15
+
+ # Boost by real audio level
+ amp = (combined + self._level * 0.6) * max_h + 3
+ amp = min(amp, max_h)
+
+ x = bar_section_x + i * (bar_w + bar_gap)
+ bar_cy = cy
+
+ # Colour gradient: centre bars brighter
+ ratio = amp / max_h
+ if ratio > 0.75:
+ color = self.RED
+ elif ratio > 0.45:
+ color = self.PURPLE
else:
- color = "#8b5cf6"
+ color = self.PURPLE2
- c.create_rectangle(x, cy - amp, x + bar_width, cy + amp,
- fill=color, outline="")
+ # Draw symmetric bar
+ c.create_rectangle(
+ x, bar_cy - amp, x + bar_w, bar_cy + amp,
+ fill=color, outline="",
+ )
+ # Rounded cap top
+ c.create_oval(
+ x, bar_cy - amp - 2, x + bar_w, bar_cy - amp + 2,
+ fill=color, outline="",
+ )
try:
if self._root:
- self._root.after(50, self._animate)
+ self._root.after(40, self._animate)
except Exception:
pass
+ # ── Helper ────────────────────────────────────────────────────
+
@staticmethod
- def _round_rect(canvas, x1, y1, x2, y2, radius, **kwargs):
- """Draw a rounded rectangle on a canvas."""
- r = radius
- canvas.create_arc(x1, y1, x1 + 2*r, y1 + 2*r, start=90, extent=90,
- style="pieslice", **kwargs)
- canvas.create_arc(x2 - 2*r, y1, x2, y1 + 2*r, start=0, extent=90,
- style="pieslice", **kwargs)
- canvas.create_arc(x1, y2 - 2*r, x1 + 2*r, y2, start=180, extent=90,
- style="pieslice", **kwargs)
- canvas.create_arc(x2 - 2*r, y2 - 2*r, x2, y2, start=270, extent=90,
- style="pieslice", **kwargs)
- canvas.create_rectangle(x1 + r, y1, x2 - r, y2, **kwargs)
- canvas.create_rectangle(x1, y1 + r, x1 + r, y2 - r, **kwargs)
- canvas.create_rectangle(x2 - r, y1 + r, x2, y2 - r, **kwargs)
+ def _round_rect(canvas, x1, y1, x2, y2, r=12, **kw):
+ """Draw a rounded rectangle on a tk Canvas."""
+ canvas.create_arc(x1, y1, x1+2*r, y1+2*r, start=90, extent=90, style="pieslice", **kw)
+ canvas.create_arc(x2-2*r, y1, x2, y1+2*r, start=0, extent=90, style="pieslice", **kw)
+ canvas.create_arc(x1, y2-2*r, x1+2*r, y2, start=180, extent=90, style="pieslice", **kw)
+ canvas.create_arc(x2-2*r, y2-2*r, x2, y2, start=270, extent=90, style="pieslice", **kw)
+ canvas.create_rectangle(x1+r, y1, x2-r, y2, **kw)
+ canvas.create_rectangle(x1, y1+r, x1+r, y2-r, **kw)
+ canvas.create_rectangle(x2-r, y1+r, x2, y2-r, **kw)
diff --git a/voxflow/recorder.py b/voxflow/recorder.py
index 21c3037..9b00de1 100644
--- a/voxflow/recorder.py
+++ b/voxflow/recorder.py
@@ -1,4 +1,4 @@
-"""VoxFlow Audio Recorder - Captures microphone input."""
+"""VoxFlow Audio Recorder - Captures microphone input with device selection."""
import threading
import numpy as np
import sounddevice as sd
@@ -15,14 +15,18 @@ def __init__(
silence_threshold: float = 0.01,
silence_duration: float = 2.0,
max_duration: float = 300.0,
+ device_index: int = -1,
on_level_change: Optional[Callable[[float], None]] = None,
+ on_device_fallback: Optional[Callable[[int], None]] = None,
):
self.sample_rate = sample_rate
self.channels = channels
self.silence_threshold = silence_threshold
self.silence_duration = silence_duration
self.max_duration = max_duration
+ self.device_index = device_index # -1 = system default
self.on_level_change = on_level_change
+ self.on_device_fallback = on_device_fallback
self._recording = False
self._audio_chunks: list[np.ndarray] = []
@@ -35,8 +39,12 @@ def __init__(
def is_recording(self) -> bool:
return self._recording
+ def set_device(self, device_index: int):
+ """Change the audio input device. Takes effect on next start()."""
+ self.device_index = device_index
+
def start(self):
- """Start recording audio from the default microphone."""
+ """Start recording audio from the selected microphone."""
if self._recording:
return
@@ -45,14 +53,42 @@ def start(self):
self._silence_counter = 0
self._has_speech = False
- self._stream = sd.InputStream(
- samplerate=self.sample_rate,
- channels=self.channels,
- dtype="float32",
- blocksize=int(self.sample_rate * 0.1), # 100ms blocks
- callback=self._audio_callback,
- )
- self._stream.start()
+ # Resolve device: -1 → None (sounddevice default)
+ device = None if self.device_index < 0 else self.device_index
+
+ try:
+ self._stream = sd.InputStream(
+ device=device,
+ samplerate=self.sample_rate,
+ channels=self.channels,
+ dtype="float32",
+ blocksize=int(self.sample_rate * 0.1), # 100ms blocks
+ callback=self._audio_callback,
+ )
+ self._stream.start()
+ except Exception as e:
+ self._recording = False
+ # Retry with default device if selected one fails
+ if device is not None:
+ print(f"Device {device} failed, falling back to default: {e}")
+ self.device_index = -1
+ # Notify app so it can update UI and config permanently
+ if self.on_device_fallback:
+ try:
+ self.on_device_fallback(-1)
+ except Exception:
+ pass
+ self._stream = sd.InputStream(
+ samplerate=self.sample_rate,
+ channels=self.channels,
+ dtype="float32",
+ blocksize=int(self.sample_rate * 0.1),
+ callback=self._audio_callback,
+ )
+ self._recording = True
+ self._stream.start()
+ else:
+ raise
def stop(self) -> Optional[np.ndarray]:
"""Stop recording and return the audio data as a numpy array."""
@@ -124,3 +160,12 @@ def list_devices() -> list[dict]:
"sample_rate": dev["default_samplerate"],
})
return input_devices
+
+ @staticmethod
+ def get_default_device_name() -> str:
+ """Get the name of the current default input device."""
+ try:
+ default = sd.query_devices(kind="input")
+ return default.get("name", "Domyślny")
+ except Exception:
+ return "Domyślny"