From a640d20f83454ac1b65a1df615e58b631fb97f17 Mon Sep 17 00:00:00 2001 From: aichristabasco Date: Sat, 28 Feb 2026 14:07:48 +0000 Subject: [PATCH 1/2] feat: add German language, translation feature, and macOS fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - config.py: add de/fr/es/it/uk languages, translate_enabled/translate_target fields - post_processor.py: German fillers, corrections, initial_prompt; refactor _remove_fillers to accept fillers set - transcriber.py: add task='translate' support (Whisper built-in offline translation → EN), multilingual flags - app.py: language dropdown now shows 7 languages, settings panel has translate toggle with explanation, flags for all languages in status/history, _on_translate_toggle callback - install_mac.sh: fix portaudio path (BREW_PREFIX), install pynput hotkey fallback, check/install python-tk, better verify error handling - INSTALL_MAC.md: add tkinter/pynput troubleshooting, translation feature section - README.md: update features table with German and translation, improve macOS section" --- INSTALL_MAC.md | 15 +++++++++- README.md | 8 ++++-- START_VOXFLOW.bat | 5 ++++ VOXFLOW.lnk | Bin 0 -> 1077 bytes install_mac.sh | 26 ++++++++++++++++- voxflow/app.py | 57 ++++++++++++++++++++++++++++++++++++-- voxflow/config.py | 15 ++++++++-- voxflow/post_processor.py | 50 +++++++++++++++++++++++++++------ voxflow/transcriber.py | 18 +++++++++--- 9 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 START_VOXFLOW.bat create mode 100644 VOXFLOW.lnk diff --git a/INSTALL_MAC.md b/INSTALL_MAC.md index 76dc956..fc68e78 100644 --- a/INSTALL_MAC.md +++ b/INSTALL_MAC.md @@ -74,8 +74,10 @@ Skrypt automatycznie wykrywa architekturę. Na Apple Silicon: |---------|-------------| | `command not found: brew` | Zainstaluj [Homebrew](https://brew.sh) ręcznie | | `portaudio` błąd | `brew install portaudio` | +| `tkinter` nie znaleziony | `brew install python-tk@3.11` | | Python za stary | `brew install python@3.11` | -| Hotkey nie działa globalnie | Dodaj do Accessibility (powyżej) | +| Hotkey nie działa globalnie | Dodaj do Accessibility (patrz sekcja poniżej) | +| `pynput` błąd | `pip install pynput` (instalowany automatycznie) | | Błąd pobierania modelu | Sprawdź połączenie internetowe | --- @@ -89,3 +91,14 @@ Skrypt automatycznie wykrywa architekturę. Na Apple Silicon: - 4 GB RAM (8 GB zalecane) - Mikrofon - Połączenie internetowe (pierwsze pobranie modelu ~500 MB) + +--- + +## 🌐 Funkcja tłumaczenia (nowość) + +VoxFlow posiada wbudowane tłumaczenie głosowe **100% offline**: +Mów po polsku, niemiecku, francusku → tekst pojawia się po **angielsku**. + +Aktywacja: **⚙ Ustawienia → 🌐 Tłumacz głos → angielski** (toggle) + +> Działa bez internetu — Whisper tłumaczy lokalnie. diff --git a/README.md b/README.md index 4fa44d0..30140f3 100644 --- a/README.md +++ b/README.md @@ -79,8 +79,9 @@ Uruchom `START_VOXFLOW.bat` z folderu portable. | 🇵🇱 | 🇬🇧 | |-----|-----| -| 🎤 Dyktowanie przytrzymując klawisz | Hold-to-record hotkey | -| 🌍 Polski + Angielski (auto-detekcja) | Polish + English (auto-detect) | +| � Dyktowanie przytrzymując klawisz | Hold-to-record hotkey | +| 🌍 Polski + Angielski + Niemiecki + więcej (auto-detekcja) | Polish + English + German + more (auto-detect) | +| 🌐 Tłumaczenie głos → Angielski (100% offline) | Voice → English translation (100% offline) | | ⚡ 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 | @@ -114,8 +115,9 @@ chmod +x install_mac.sh ./install_mac.sh ``` +> Skrypt automatycznie instaluje `portaudio`, `pynput` (globalny hotkey) i sprawdza Tkinter. > Globalny hotkey wymaga uprawnień Accessibility w System Settings. -> Szczegóły: [INSTALL_MAC.md](INSTALL_MAC.md) +> Szczegóły i rozwiązywanie problemów: [INSTALL\_MAC.md](INSTALL_MAC.md) --- diff --git a/START_VOXFLOW.bat b/START_VOXFLOW.bat new file mode 100644 index 0000000..5ab532f --- /dev/null +++ b/START_VOXFLOW.bat @@ -0,0 +1,5 @@ +@echo off +cd /d "%~dp0" +call venv\Scripts\activate.bat +start "" pythonw -m voxflow.main +if errorlevel 1 start "" python -m voxflow.main diff --git a/VOXFLOW.lnk b/VOXFLOW.lnk new file mode 100644 index 0000000000000000000000000000000000000000..e43bdd4582eeee6c9d553e84a0adc4a2731aacd1 GIT binary patch literal 1077 zcma)5Ur1A76#vbvrj$)*l9IUs!6?_Xd}uM)Y;8u9D^A>y3;xTcxty_~lJqJB?kRc* zGNddC>S0kti6PaC56S33Yzg`nwZcX(1wQmU*DVu;JeS`&-*>)qf9HJXp6daiE6kz> zW;6}gC+K0~dL;I4DLJvIi_PwO>z-WHW!Dz({KkEBEe^W0&6wXPL|MbK5;uLd_89Ih z=yA+q87syKr&Z0w{9~I@rR{ubSJZ%k&`=;48iXA=$i~WRMU9NLs8dlhV-nUq)N4hq z6I{F9hXI5zgaCrHgFGri1l5oNedkeDC1R@y5rrrsb`Z}l+3px< z-H!TmUa%+WE32D}gpvH}HnQi!2`eKv_Mvo3ugZGEe7ab5c+)#(^q`YnY+I{#+)pcV zu`Apg3H4XD2Wmnc{XyvGKmQm$Rp5DQT5ww4e_46+@>Se#Oy%FdiIVGytNH&spJ9t~ zfbsL>uJSf`zgwmgXK}+l`73Um2nUW-N=`{!M(iavUd@CG?Q%yYM3Y%+A+n^(U#z%G z3=dLSZKHRSk89}9l?Wj!Ahz=@_cLxE!7-#ErGQX7UdkDG6!^@o&wp$/dev/null || echo "/usr/local") + if brew list portaudio &>/dev/null; then echo -e "${GREEN} ✅ portaudio już zainstalowany${NC}" else @@ -71,6 +74,9 @@ else echo -e "${GREEN} ✅ portaudio zainstalowany${NC}" fi +# Export portaudio path so sounddevice can find the dylib +export DYLD_LIBRARY_PATH="${BREW_PREFIX}/lib:${DYLD_LIBRARY_PATH:-}" + # ─── Check Python 3.9+ ───────────────────────────────────────── echo "" echo "══════════════════════════════════════════════════════" @@ -129,8 +135,18 @@ echo "" pip install --upgrade pip --quiet pip install -r requirements.txt --quiet +# Install pynput as macOS hotkey fallback (keyboard lib needs root on macOS) +pip install pynput --quiet 2>/dev/null || true + echo -e "${GREEN} ✅ Pakiety zainstalowane${NC}" +# Ensure Tkinter is available — install python-tk via Homebrew if missing +python -c "import tkinter" 2>/dev/null || { + echo -e "${YELLOW} ℹ️ Tkinter nie znaleziony. Instaluję python-tk@3.11...${NC}" + brew install python-tk@3.11 2>/dev/null || brew install python-tk 2>/dev/null || true + echo -e " ⚠️ Jeśli Tkinter nadal nie działa, uruchom: brew install python-tk@3.11" +} + # ─── Verify ───────────────────────────────────────────────────── echo "" echo "══════════════════════════════════════════════════════" @@ -138,7 +154,15 @@ echo " KROK 6: Weryfikacja instalacji..." echo "══════════════════════════════════════════════════════" echo "" -python -m voxflow.main --test +if python -m voxflow.main --test; then + echo -e "${GREEN} ✅ Weryfikacja zakończona pomyślnie${NC}" +else + echo -e "${YELLOW} ⚠️ Weryfikacja z ostrzeżeniami — sprawdź błędy powyżej.${NC}" + echo " Typowe problemy:" + echo " • tkinter: brew install python-tk@3.11" + echo " • portaudio: brew install portaudio" + echo " • HotKey: dodaj Terminal do Accessibility w Ustawieniach" +fi # ─── Create launcher ───────────────────────────────────────────── echo "" diff --git a/voxflow/app.py b/voxflow/app.py index 3bae75f..033e059 100644 --- a/voxflow/app.py +++ b/voxflow/app.py @@ -479,7 +479,7 @@ def _build_quick_controls(self): text_color=C["txt3"]).pack(anchor="w") self.lang_var = ctk.StringVar(value=self.config.language) ctk.CTkOptionMenu( - lf, values=["auto", "pl", "en"], + lf, values=["auto", "pl", "en", "de", "fr", "es", "it", "uk"], variable=self.lang_var, font=ctk.CTkFont(size=11), fg_color=C["bg_input"], @@ -637,6 +637,28 @@ def opt_row(parent, label, values, var, cmd, width=130): self.autostart_var = ctk.BooleanVar(value=is_autostart_enabled()) sw_row(inner, "🚀 Uruchamiaj z Windows", self.autostart_var, self._on_autostart_toggle) + # ── Translation section ──────────────────────────────── + ctk.CTkFrame(inner, fg_color=C["border"], height=1).pack(fill="x", pady=(10, 8)) + + ctk.CTkLabel( + inner, text="🌐 Tłumaczenie (Whisper → EN)", + font=ctk.CTkFont(size=12, weight="bold"), + text_color=C["txt2"], + ).pack(anchor="w", pady=(0, 4)) + + self.translate_var = ctk.BooleanVar(value=self.config.translate_enabled) + sw_row(inner, "🌐 Tłumacz głos → angielski", self.translate_var, self._on_translate_toggle) + + ctk.CTkLabel( + inner, + text=" ℹ️ Mówisz po polsku, francusku lub innych językach\n" + " — transkrypcja pojawia się w języku angielskim.\n" + " Działa 100% offline (Whisper native).", + font=ctk.CTkFont(size=10), + text_color=C["txt3"], + justify="left", + ).pack(anchor="w", pady=(2, 0)) + # ── Color theme selector ─────────────────────── ctk.CTkFrame(inner, fg_color=C["border"], height=1).pack(fill="x", pady=(10, 8)) @@ -918,12 +940,14 @@ def _stop_rec(self): def _transcribe(self, audio: np.ndarray): try: + task = "translate" if self.config.translate_enabled else "transcribe" result = self.transcriber.transcribe( audio, language=self.lang_var.get(), beam_size=self.config.beam_size, vad_enabled=self.config.vad_enabled, auto_correct=self.config.auto_correct, + task=task, on_progress=lambda m: self.after( 0, lambda msg=m: self.status.configure(text=msg) ), @@ -959,13 +983,21 @@ def _on_done(self, result: dict): ).start() lang = result.get("language", "?") - flag = "🇵🇱" if lang == "pl" else "🇬🇧" if lang == "en" else "🌍" + translated = result.get("translated", False) + _LANG_FLAGS = { + "pl": "🇵🇱", "en": "🇬🇧", "de": "🇩🇪", + "fr": "🇫🇷", "es": "🇪🇸", "it": "🇮🇹", + "uk": "🇺🇦", + } + flag = _LANG_FLAGS.get(lang, "🌍") dur = result.get("duration", 0) extras = [] if self.config.auto_type_enabled: extras.append("✍️") if self.config.auto_copy_to_clipboard: extras.append("📋") + if translated: + extras.append("🌐→EN") extra_str = " • " + " ".join(extras) if extras else "" self.status.configure( text=f"✅ {flag} {lang.upper()} • {dur:.1f}s{extra_str}", @@ -1016,7 +1048,12 @@ def _refresh_history(self): ) row.pack(fill="x", pady=2) row.pack_propagate(False) - flag = "🇵🇱" if e["language"] == "pl" else "🇬🇧" if e["language"] == "en" else "🌍" + _LANG_FLAGS = { + "pl": "🇵🇱", "en": "🇬🇧", "de": "🇩🇪", + "fr": "🇫🇷", "es": "🇪🇸", "it": "🇮🇹", + "uk": "🇺🇦", + } + flag = _LANG_FLAGS.get(e["language"], "🌍") preview = e["text"][:44] + ("…" if len(e["text"]) > 44 else "") ctk.CTkLabel( row, @@ -1097,6 +1134,20 @@ def _on_autostart_toggle(self): msg = "🚀 Auto-start włączony" if enabled else "🔕 Auto-start wyłączony" self.status.configure(text=msg, text_color=C["ok"]) + def _on_translate_toggle(self): + self.config.translate_enabled = self.translate_var.get() + self.config.save() + if self.config.translate_enabled: + self.status.configure( + text="🌐 Tłumaczenie włączone — głos → angielski", + text_color=C["accent2"], + ) + else: + self.status.configure( + text="🌐 Tłumaczenie wyłączone", + text_color=C["txt2"], + ) + def _reload_model(self, sz): try: self.transcriber.load_model( diff --git a/voxflow/config.py b/voxflow/config.py index da626a3..587c34b 100644 --- a/voxflow/config.py +++ b/voxflow/config.py @@ -18,7 +18,7 @@ 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_LANGUAGES = {"auto", "pl", "en", "de", "fr", "es", "it", "uk"} # 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"} @@ -61,6 +61,8 @@ 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 == "translate_target" and value not in _VALID_LANGUAGES: + validated[key] = default_val elif key == "hotkey": # Accept any non-empty string — hotkey picker sets arbitrary keys s = str(value).strip().lower() @@ -98,7 +100,7 @@ class VoxFlowConfig: """Application configuration.""" # Model settings model_size: str = "small" - language: str = "auto" # "auto", "pl", "en" + language: str = "auto" # "auto", "pl", "en", "de", "fr", "es", "it", "uk" device: str = "cpu" # "cpu" or "cuda" compute_type: str = "int8" # "int8" for CPU, "float16" for GPU @@ -136,6 +138,10 @@ class VoxFlowConfig: vad_silence_ms: int = 300 auto_correct: bool = True + # Translation (Whisper built-in translate task → English) + translate_enabled: bool = False + translate_target: str = "en" # Currently only "en" supported (Whisper limitation) + def save(self): """Save configuration to JSON file.""" config_path = get_config_dir() / "config.json" @@ -170,6 +176,11 @@ def available_languages(self) -> dict: "auto": "🌍 Auto-detect", "pl": "🇵🇱 Polski", "en": "🇬🇧 English", + "de": "🇩🇪 Deutsch", + "fr": "🇫🇷 Français", + "es": "🇪🇸 Español", + "it": "🇮🇹 Italiano", + "uk": "🇺🇦 Українська", } @property diff --git a/voxflow/post_processor.py b/voxflow/post_processor.py index d43112f..86f81d3 100644 --- a/voxflow/post_processor.py +++ b/voxflow/post_processor.py @@ -1,10 +1,10 @@ """VoxFlow Post-Processor — Auto-correction for transcribed text. -Fixes common Whisper transcription errors, especially for Polish: +Fixes common Whisper transcription errors for Polish, English, German, and more: - Removes repeated phrases (Whisper hallucination) -- Fixes capitalization (first letter of sentences) +- Fixes capitalization (first letter of sentences) - Cleans up punctuation -- Fixes common Polish word confusions +- Fixes common word confusions per language - Removes filler words/sounds """ import re @@ -46,6 +46,25 @@ r"\bgotta\b": "got to", } +# German filler words/sounds +GERMAN_FILLERS = { + "äh", "ähm", "hm", "öh", "öhm", "hmm", "ehm", +} + +# Common German Whisper corrections +GERMAN_CORRECTIONS = { + # Common split compounds + r"\bauch\s+wenn\b": "auch wenn", + r"\bzu\s+sammen\b": "zusammen", + r"\bdes\s+halb\b": "deshalb", + r"\bdem\s+nach\b": "demnach", + r"\bober\s+halb\b": "oberhalb", + r"\bunter\s+halb\b": "unterhalb", + # Whisper artifacts + r"\[.*?\]": "", + r"\(.*?\)": "", +} + def post_process( text: str, @@ -83,8 +102,11 @@ def post_process( result = _fix_repetitions(result) # 3. Remove filler words - if remove_fillers and language in ("pl", "auto"): - result = _remove_fillers(result) + if remove_fillers: + if language in ("pl", "auto"): + result = _remove_fillers(result, POLISH_FILLERS) + if language == "de": + result = _remove_fillers(result, GERMAN_FILLERS) # 4. Apply language-specific corrections if apply_corrections: @@ -92,6 +114,8 @@ def post_process( result = _apply_corrections(result, POLISH_CORRECTIONS) if language in ("en", "auto"): result = _apply_corrections(result, ENGLISH_CORRECTIONS) + if language == "de": + result = _apply_corrections(result, GERMAN_CORRECTIONS) # 5. Fix punctuation if fix_punctuation: @@ -147,9 +171,9 @@ def _fix_repetitions(text: str) -> str: return result -def _remove_fillers(text: str) -> str: - """Remove filler words/sounds.""" - for filler in POLISH_FILLERS: +def _remove_fillers(text: str, fillers: set) -> str: + """Remove filler words/sounds from a given set.""" + for filler in fillers: # Remove filler as standalone word (with possible punctuation) pattern = r'\b' + re.escape(filler) + r'[,.]?\s*' text = re.sub(pattern, '', text, flags=re.IGNORECASE) @@ -227,6 +251,12 @@ def _final_cleanup(text: str) -> str: "The text uses proper grammar and punctuation." ) +GERMAN_INITIAL_PROMPT = ( + "Transkription einer Aufnahme auf Deutsch. " + "Der Text enthält korrekte deutsche Umlaute: ä, ö, ü, Ä, Ö, Ü, ß. " + "Die Sätze sind grammatikalisch und orthografisch korrekt." +) + AUTO_INITIAL_PROMPT = ( "Transkrypcja nagrania. Tekst może być po polsku lub angielsku. " "Polski tekst zawiera poprawne znaki diakrytyczne: ą, ć, ę, ł, ń, ó, ś, ź, ż." @@ -235,7 +265,7 @@ def _final_cleanup(text: str) -> str: def get_initial_prompt(language: str) -> str: """Get the initial prompt for Whisper based on language. - + The initial_prompt biases Whisper towards producing correct output in the target language with proper diacritics and grammar. """ @@ -243,5 +273,7 @@ def get_initial_prompt(language: str) -> str: return POLISH_INITIAL_PROMPT elif language == "en": return ENGLISH_INITIAL_PROMPT + elif language == "de": + return GERMAN_INITIAL_PROMPT else: return AUTO_INITIAL_PROMPT diff --git a/voxflow/transcriber.py b/voxflow/transcriber.py index 09098a5..12658e8 100644 --- a/voxflow/transcriber.py +++ b/voxflow/transcriber.py @@ -77,20 +77,22 @@ def transcribe( beam_size: int = 5, vad_enabled: bool = True, auto_correct: bool = True, + task: str = "transcribe", on_progress: Optional[callable] = None, ) -> dict: """Transcribe audio data to text with maximum quality. Args: audio_data: numpy array of audio samples (float32, 16kHz mono) - language: Language code ("pl", "en") or "auto" for detection + language: Language code ("pl", "en", "de", ...) or "auto" for detection beam_size: Beam search width (higher = more accurate, slower) vad_enabled: Use Voice Activity Detection filtering auto_correct: Apply post-processing auto-correction + task: "transcribe" (default) or "translate" (Whisper translates to English) on_progress: Callback for progress updates Returns: - dict with keys: text, raw_text, language, segments, duration + dict with keys: text, raw_text, language, segments, duration, translated """ if not self._model_loaded or self._model is None: raise RuntimeError("Model nie jest załadowany. Wywołaj load_model() najpierw.") @@ -122,6 +124,7 @@ def transcribe( "log_prob_threshold": -1.0, "no_speech_threshold": 0.6, "word_timestamps": False, + "task": task, # "transcribe" or "translate" (-> EN) } if lang_code: @@ -180,12 +183,19 @@ def transcribe( "language_probability": info.language_probability, "segments": segments, "duration": info.duration, + "translated": task == "translate", } if on_progress: - flag = "🇵🇱" if info.language == "pl" else "🇬🇧" if info.language == "en" else "🌍" + _LANG_FLAGS = { + "pl": "🇵🇱", "en": "🇬🇧", "de": "🇩🇪", + "fr": "🇫🇷", "es": "🇪🇸", "it": "🇮🇹", + "uk": "🇺🇦", + } + flag = _LANG_FLAGS.get(info.language, "🌍") prob = info.language_probability * 100 - on_progress(f"✅ {flag} {info.language.upper()} ({prob:.0f}%) • {info.duration:.1f}s") + tr_suffix = " →🇬🇧" if task == "translate" else "" + on_progress(f"✅ {flag} {info.language.upper()} ({prob:.0f}%){tr_suffix} • {info.duration:.1f}s") return result From 0ad64cd613c3e7af5d7b9e4541669c8274b8d258 Mon Sep 17 00:00:00 2001 From: aichristabasco Date: Sat, 28 Feb 2026 16:26:02 +0000 Subject: [PATCH 2/2] feat: desktop shortcut + portable build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_shortcut.py: rewritten to support bat-launcher shortcut (post-install) and EXE shortcut (portable). Uses WScript.Shell COM or PowerShell -File (temp .ps1, utf-8-sig, single-quoted paths) as fallback. WindowStyle=7 hides cmd flash. - install.bat: added KROK 5 — auto-creates desktop shortcut after install - CREATE_PORTABLE.bat: complete rewrite — venv-based portable (copies venv + sources, no PyInstaller needed), creates desktop shortcut, writes README_PORTABLE.txt, optional ZIP. Output: portable\VoxFlow_Portable_v1.2.0\" --- CREATE_PORTABLE.bat | 134 ++++++++++++++--------- install.bat | 17 ++- voxflow/create_shortcut.py | 213 ++++++++++++++++++++++++++++--------- 3 files changed, 259 insertions(+), 105 deletions(-) diff --git a/CREATE_PORTABLE.bat b/CREATE_PORTABLE.bat index 81f68bb..acc9282 100644 --- a/CREATE_PORTABLE.bat +++ b/CREATE_PORTABLE.bat @@ -1,17 +1,17 @@ @echo off +chcp 65001 >nul 2>&1 cd /d "%~dp0" title VoxFlow - Tworzenie wersji Portable echo. echo ============================================================ -echo VoxFlow - Tworzenie wersji Portable +echo VoxFlow - Tworzenie wersji Portable (venv-based) echo by AI Evolution Polska echo ============================================================ echo. -set VER=1.1.0 -set PORTABLE_DIR=dist\VoxFlow_Portable_v%VER% -set PORTABLE_APP=%PORTABLE_DIR%\VoxFlow +set VER=1.2.0 +set PORTABLE_DIR=portable\VoxFlow_Portable_v%VER% REM --- Sprawdz venv --- if not exist "venv\Scripts\activate.bat" ( @@ -20,84 +20,113 @@ if not exist "venv\Scripts\activate.bat" ( echo. pause & exit /b 1 ) -call venv\Scripts\activate.bat -REM --- Krok 1: Zbuduj EXE jesli nie istnieje --- -echo [1/4] Sprawdzam czy VoxFlow.exe jest zbudowany... -if exist "dist\VoxFlow\VoxFlow.exe" ( - echo [OK] VoxFlow.exe juz istnieje - pomijam build -) else ( - echo Budowanie VoxFlow.exe przez PyInstaller... - echo (To moze potrwac 3-6 minut) - pip install pyinstaller --quiet --disable-pip-version-check - pyinstaller VoxFlow.spec --noconfirm --clean - if %errorlevel% neq 0 ( - echo [BLAD] Build nie powiodl sie! - pause & exit /b 1 - ) - if not exist "dist\VoxFlow\VoxFlow.exe" ( - echo [BLAD] Nie znaleziono dist\VoxFlow\VoxFlow.exe - pause & exit /b 1 - ) - echo [OK] VoxFlow.exe zbudowany -) - -REM --- Krok 2: Wyczysc stary portable folder --- -echo. -echo [2/4] Przygotowanie katalogu portable... +REM ============================================================== +REM KROK 1: Przygotuj katalog portable +REM ============================================================== +echo [1/5] Przygotowanie katalogu portable... if exist "%PORTABLE_DIR%" ( - echo Usuwam stary folder %PORTABLE_DIR%... + echo Usuwam stary folder %PORTABLE_DIR%... rmdir /s /q "%PORTABLE_DIR%" ) mkdir "%PORTABLE_DIR%" +mkdir "%PORTABLE_DIR%\assets" + +REM ============================================================== +REM KROK 2: Skopiuj zrodla VoxFlow +REM ============================================================== +echo [2/5] Kopiowanie zrodel VoxFlow... +xcopy "voxflow\*" "%PORTABLE_DIR%\voxflow\" /E /I /Q /Y +if %errorlevel% neq 0 ( + echo [BLAD] Blad kopiowania zrodel! + pause & exit /b 1 +) -REM --- Krok 3: Kopiuj pliki --- +REM --- Skopiuj ikone i zasoby --- +if exist "assets\voxflow.ico" copy /Y "assets\voxflow.ico" "%PORTABLE_DIR%\assets\" >nul +if exist "assets\voxflow_256.png" copy /Y "assets\voxflow_256.png" "%PORTABLE_DIR%\assets\" >nul +if exist "requirements.txt" copy /Y "requirements.txt" "%PORTABLE_DIR%\" >nul + +echo [OK] Zrodla skopiowane + +REM ============================================================== +REM KROK 3: Skopiuj i zregeneruj srodowisko venv (site-packages) +REM ============================================================== echo. -echo [3/4] Kopiowanie plikow VoxFlow... -xcopy "dist\VoxFlow\*" "%PORTABLE_APP%\" /E /I /Q +echo [3/5] Kopiowanie srodowiska Python (venv)... +echo (to moze chwile potrwac) +xcopy "venv\*" "%PORTABLE_DIR%\venv\" /E /I /Q /Y if %errorlevel% neq 0 ( - echo [BLAD] Blad kopiowania plikow! + echo [BLAD] Blad kopiowania venv! pause & exit /b 1 ) +echo [OK] venv skopiowany + +REM ============================================================== +REM KROK 4: Stworz launcher START_VOXFLOW.bat w katalogu portable +REM ============================================================== +echo. +echo [4/5] Tworzenie launchera i skrotu na pulpicie... -REM --- Stworz launcher START_VOXFLOW.bat --- ( echo @echo off -echo cd /d "%%~dp0VoxFlow" -echo start "" "VoxFlow.exe" +echo chcp 65001 ^>nul 2^>^&1 +echo cd /d "%%~dp0" +echo call venv\Scripts\activate.bat +echo start "" pythonw -m voxflow.main +echo if errorlevel 1 start "" python -m voxflow.main ) > "%PORTABLE_DIR%\START_VOXFLOW.bat" -REM --- Stworz README_PORTABLE.txt --- +REM --- Skrot na pulpicie wskazujacy na launcher portable --- +call venv\Scripts\activate.bat +set PORTABLE_ABS=%~dp0%PORTABLE_DIR% +venv\Scripts\python.exe -c "import sys; sys.path.insert(0,'%~dp0'); from voxflow.create_shortcut import create_desktop_shortcut_for_bat; create_desktop_shortcut_for_bat(r'%PORTABLE_ABS%')" +if %errorlevel% equ 0 ( + echo [OK] Skrot VoxFlow Portable pojawil sie na pulpicie! +) else ( + echo [UWAGA] Skrotu nie udalo sie utworzyc automatycznie. + echo Mozesz recznie przeniesc START_VOXFLOW.bat na pulpit. +) + +REM ============================================================== +REM KROK 5: Stworz README_PORTABLE.txt +REM ============================================================== ( -echo VoxFlow %VER% - Wersja Portable -echo ================================ +echo VoxFlow v%VER% - Wersja Portable +echo ================================= echo. echo JAK URUCHOMIC: -echo Kliknij dwukrotnie START_VOXFLOW.bat -echo (lub uruchom VoxFlow\VoxFlow.exe bezposrednio) +echo 1. Kliknij skrot "VoxFlow" na Pulpicie +echo lub +echo 2. Kliknij dwukrotnie START_VOXFLOW.bat w tym folderze +echo. +echo CO NOWEGO: +echo - Jezyki: Polski + Angielski + Niemiecki + Francuski + inne +echo - Tlumaczenie glosu -^> Angielski (100%% offline) +echo - Skrot na Pulpicie tworzony automatycznie echo. echo JAK DZIALA: -echo - Przytrzymaj F2 i mow - pojawi sie animacja nagrywania na dole ekranu -echo - Zwolnij F2 - tekst zostanie wklejony w aktywnym oknie -echo - Kliknij przycisk klawisza w UI aby zmienic skrot klawiszowy -echo - Ikona w zasobniku - kliknij prawym przyciskiem dla opcji +echo - Przytrzymaj F2 i mow ^(pojawi sie animacja fal^) +echo - Zwolnij F2 ^- tekst zostanie wklejony w aktywnym oknie +echo - Kliknij przycisk klawisza w UI aby zmienic skrot +echo - Ustawienia: przelacznik Tlumaczenie w panelu Ustawien echo. echo WYMAGANIA: echo - Windows 10/11 (64-bit) echo - Mikrofon echo - Internet TYLKO przy pierwszym uruchomieniu -echo (pobranie modelu AI ~500 MB) +echo ^(pobranie modelu AI ~500 MB, zapisywany w APPDATA\VoxFlow\^) echo. echo WAZNE: -echo - NIE przenosic samego VoxFlow.exe bez calego folderu VoxFlow\ -echo - Caly folder VoxFlow_Portable_v%VER% mozna przeniesc gdzie chcesz +echo - NIE przenosic samego folderu voxflow\ bez reszty +echo - Caly folder VoxFlow_Portable_v%VER%\ mozna przeniesc gdzie chcesz echo - Ustawienia 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 [5/5] Gotowe! echo. REM --- Zapytaj czy zipowac --- @@ -106,10 +135,10 @@ if /i "%DOZIP%"=="n" goto :done echo. echo Pakuje do ZIP... -powershell -Command "Compress-Archive -Path '%PORTABLE_DIR%' -DestinationPath 'dist\VoxFlow_Portable_v%VER%.zip' -Force" +powershell -Command "Compress-Archive -Path '%PORTABLE_DIR%' -DestinationPath 'portable\VoxFlow_Portable_v%VER%.zip' -Force" if %errorlevel% equ 0 ( - echo [OK] ZIP stworzony: dist\VoxFlow_Portable_v%VER%.zip + echo [OK] ZIP stworzony: portable\VoxFlow_Portable_v%VER%.zip ) else ( echo [UWAGA] Nie udalo sie stworzyc ZIP. Folder istnieje: %PORTABLE_DIR% ) @@ -119,8 +148,9 @@ echo. echo ============================================================ echo [OK] Wersja Portable gotowa! echo. -echo Folder: %PORTABLE_DIR% +echo Folder: %PORTABLE_DIR% echo Launcher: START_VOXFLOW.bat +echo Skrot: Pulpit\VoxFlow.lnk echo ============================================================ echo. diff --git a/install.bat b/install.bat index aece315..db33e8b 100644 --- a/install.bat +++ b/install.bat @@ -119,12 +119,27 @@ echo if errorlevel 1 start "" python -m voxflow.main echo [OK] Plik START_VOXFLOW.bat stworzony! +echo. +echo ------------------------------------------------------------ +echo KROK 5: Tworzenie skrotu na pulpicie... +echo ------------------------------------------------------------ +echo. + +venv\Scripts\python.exe -c "import sys; sys.path.insert(0,'%~dp0'); from voxflow.create_shortcut import create_desktop_shortcut_for_bat; create_desktop_shortcut_for_bat('%~dp0'.rstrip(chr(92)))" +if %errorlevel% neq 0 ( + echo [UWAGA] Skrotu nie udalo sie utworzyc automatycznie. + echo Mozesz recznie przeniesc START_VOXFLOW.bat na pulpit. +) else ( + echo [OK] Skrot VoxFlow pojawil sie na pulpicie! +) + echo. echo ============================================================ echo [OK] VoxFlow zainstalowany pomyslnie! echo. echo Uruchom aplikacje: -echo --^> Kliknij dwukrotnie START_VOXFLOW.bat +echo --^> Kliknij skrot VoxFlow na Pulpicie +echo --^> lub dwukrotnie kliknij START_VOXFLOW.bat echo ============================================================ echo. diff --git a/voxflow/create_shortcut.py b/voxflow/create_shortcut.py index e6a0795..09c8721 100644 --- a/voxflow/create_shortcut.py +++ b/voxflow/create_shortcut.py @@ -1,85 +1,194 @@ -"""Create desktop shortcut for VoxFlow. +"""Create a desktop shortcut for VoxFlow — works for both venv and EXE builds. Built by AI Evolution Polska """ import os import sys import re +import subprocess def _sanitize_path(path: str) -> str: """Sanitize a file path to prevent injection in PowerShell commands.""" - # Remove characters that could be used for PowerShell injection return re.sub(r'[`$\{\}\[\];&|]', '', path) -def create_shortcut(): - """Create a desktop shortcut for VoxFlow.exe.""" +def _get_desktop() -> str: + """Return the current user's Desktop path (Windows).""" try: + import ctypes import ctypes.wintypes - - # Get desktop path CSIDL_DESKTOP = 0 buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_DESKTOP, None, 0, buf) - desktop = buf.value + if buf.value: + return buf.value except Exception: - desktop = os.path.join(os.path.expanduser("~"), "Desktop") + pass + return os.path.join(os.path.expanduser("~"), "Desktop") - # Determine exe path - script_dir = os.path.dirname(os.path.abspath(__file__)) - project_dir = os.path.dirname(script_dir) - - exe_path = os.path.join(project_dir, "dist", "VoxFlow", "VoxFlow.exe") - icon_path = os.path.join(project_dir, "assets", "voxflow.ico") + +def _create_via_powershell(shortcut_path: str, target: str, workdir: str, + icon_path: str, description: str, args: str = "") -> bool: + """Create a .lnk shortcut by writing and running a temp PowerShell script.""" + import tempfile + + # Build PS1 content — use single-quoted PS strings for paths (no backslash escaping needed) + # For Arguments we use double-quoted PS string with escaped inner quotes + ps_lines = [ + "$ws = New-Object -ComObject WScript.Shell", + f"$sc = $ws.CreateShortcut('{shortcut_path}')", + f"$sc.TargetPath = '{target}'", + f"$sc.WorkingDirectory = '{workdir}'", + ] + if args: + # args may contain double quotes; escape them as ` in PS double-quoted string + escaped_args = args.replace('"', '`"') + ps_lines.append(f'$sc.Arguments = "{escaped_args}"') + if os.path.exists(icon_path): + ps_lines.append(f"$sc.IconLocation = '{icon_path},0'") + ps_lines += [ + f"$sc.Description = '{description}'", + "$sc.WindowStyle = 7", + "$sc.Save()", + ] + + ps_script = "\r\n".join(ps_lines) + + # Write to a temp file — use utf-8-sig (BOM) so PowerShell reads it correctly + fd, ps_path = tempfile.mkstemp(suffix=".ps1", prefix="voxflow_sc_") + try: + with os.fdopen(fd, "w", encoding="utf-8-sig") as f: + f.write(ps_script) + + result = subprocess.run( + [ + "powershell", + "-NoProfile", "-NonInteractive", + "-ExecutionPolicy", "Bypass", + "-File", ps_path, + ], + capture_output=True, text=True, timeout=20, + ) + return result.returncode == 0 + except Exception as e: + print(f" [PS fallback error] {e}") + return False + finally: + try: + os.unlink(ps_path) + except OSError: + pass + + + +def create_desktop_shortcut_for_bat(project_dir: str) -> bool: + """Create a desktop shortcut that launches START_VOXFLOW.bat. + + This is the primary shortcut created after install.bat runs. + It doesn't require an EXE — just the venv + source. + """ + desktop = _get_desktop() shortcut_path = os.path.join(desktop, "VoxFlow.lnk") + launcher_bat = os.path.join(project_dir, "START_VOXFLOW.bat") + icon_path = os.path.join(project_dir, "assets", "voxflow.ico") + cmd_exe = os.path.join(os.environ.get("SystemRoot", r"C:\Windows"), "System32", "cmd.exe") + # Launch minimized (/c runs and closes the interim window; start hides it) + args = f'/c "{launcher_bat}"' + + if not os.path.exists(launcher_bat): + print(f"❌ START_VOXFLOW.bat not found: {launcher_bat}") + return False + + # --- Try win32com first (most reliable) --- + try: + import win32com.client # type: ignore + shell = win32com.client.Dispatch("WScript.Shell") + sc = shell.CreateShortCut(shortcut_path) + sc.TargetPath = cmd_exe + sc.Arguments = args + sc.WorkingDirectory = project_dir + sc.Description = "VoxFlow — Lokalne dyktowanie głosem • AI Evolution Polska" + sc.WindowStyle = 7 # minimized (hides cmd flash) + if os.path.exists(icon_path): + sc.IconLocation = icon_path + sc.save() + print(f"✅ Skrót na pulpicie: {shortcut_path}") + return True + except ImportError: + pass + + # --- Fallback: PowerShell --- + ok = _create_via_powershell( + shortcut_path=shortcut_path, + target=cmd_exe, + workdir=project_dir, + icon_path=icon_path, + description="VoxFlow — Lokalne dyktowanie głosem", + args=args, + ) + if ok: + print(f"✅ Skrót na pulpicie: {shortcut_path}") + else: + print(f"⚠️ Nie udało się stworzyć skrótu. Możesz ręcznie przeciągnąć START_VOXFLOW.bat na pulpit.") + return ok + + +def create_desktop_shortcut_for_exe(project_dir: str) -> bool: + """Create a desktop shortcut pointing directly at VoxFlow.exe (portable/EXE build).""" + desktop = _get_desktop() + shortcut_path = os.path.join(desktop, "VoxFlow.lnk") + exe_path = os.path.join(project_dir, "VoxFlow.exe") + icon_path = os.path.join(project_dir, "assets", "voxflow.ico") if not os.path.exists(exe_path): - print(f"❌ VoxFlow.exe not found at: {exe_path}") - print(" Run BUILD_EXE.bat first!") + print(f"❌ VoxFlow.exe nie znaleziony: {exe_path}") return False try: - # Use COM to create shortcut - import win32com.client + import win32com.client # type: ignore shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortCut(shortcut_path) - shortcut.TargetPath = exe_path - shortcut.WorkingDirectory = os.path.dirname(exe_path) + sc = shell.CreateShortCut(shortcut_path) + sc.TargetPath = exe_path + sc.WorkingDirectory = project_dir + sc.Description = "VoxFlow — Lokalne dyktowanie głosem" if os.path.exists(icon_path): - shortcut.IconLocation = icon_path - shortcut.Description = "VoxFlow — Lokalne rozpoznawanie mowy • AI Evolution Polska" - shortcut.save() - print(f"✅ Shortcut created: {shortcut_path}") + sc.IconLocation = icon_path + sc.save() + print(f"✅ Skrót na pulpicie: {shortcut_path}") return True except ImportError: - # Fallback: use PowerShell with sanitized paths - safe_shortcut = _sanitize_path(shortcut_path) - safe_exe = _sanitize_path(exe_path) - safe_workdir = _sanitize_path(os.path.dirname(exe_path)) - safe_icon = _sanitize_path(icon_path) - - ps_script = f''' -$ws = New-Object -ComObject WScript.Shell -$sc = $ws.CreateShortcut("{safe_shortcut}") -$sc.TargetPath = "{safe_exe}" -$sc.WorkingDirectory = "{safe_workdir}" -$sc.IconLocation = "{safe_icon}" -$sc.Description = "VoxFlow" -$sc.Save() -''' - import subprocess - result = subprocess.run( - ["powershell", "-Command", ps_script], - capture_output=True, text=True, - ) - if result.returncode == 0: - print(f"✅ Shortcut created: {shortcut_path}") - return True - else: - print(f"❌ Error: {result.stderr}") - return False + pass + + ok = _create_via_powershell( + shortcut_path=shortcut_path, + target=exe_path, + workdir=project_dir, + icon_path=icon_path, + description="VoxFlow — Lokalne dyktowanie głosem", + ) + if ok: + print(f"✅ Skrót na pulpicie: {shortcut_path}") + else: + print("⚠️ Nie udało się stworzyć skrótu do EXE.") + return ok + + +# ── Legacy entry point (backwards compat) ────────────────────────────────────── +def create_shortcut(): + """Legacy: create EXE shortcut. Called from old build scripts.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_dir = os.path.dirname(script_dir) + exe_dir = os.path.join(project_dir, "dist", "VoxFlow") + return create_desktop_shortcut_for_exe(exe_dir) if __name__ == "__main__": - create_shortcut() + # Called directly — prefer bat launcher if venv exists, else EXE + script_dir = os.path.dirname(os.path.abspath(__file__)) + project_dir = os.path.dirname(script_dir) + + if os.path.exists(os.path.join(project_dir, "START_VOXFLOW.bat")): + create_desktop_shortcut_for_bat(project_dir) + else: + create_shortcut()