+1

![dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>](/assets/img/avatar_default.png)







sevenjay
GitHub
Sbenazar
jay
Jatin K Malik
Fakhareddine
Sisyphus
dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
parko636
juanfradb
Pep Locanto
84d2e1a929
* feat: add remote API speech recognition engine with installation and configuration support. * fix: reformatted by black * fix: unit test in test_main_initializes_components * fix typo * Apply suggestion from @jatinkrmalik Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> * feat: Validate remote API URL format, enhance connection testing, update default remote API endpoint. * fix typo * feat: add remote API recognition manager test suite * fix: add Python 3.14 support and lxml>=6.1.0 (fixes #404) - Add Python 3.14 to classifiers - Add lxml>=6.1.0 to dependencies to fix install on Python 3.14 (CachyOS, Arch) - Update black target-version to include py314 Fixes #404 Closes #405 * fix(install): validate pyproject.toml/setup.py content before entering local repo mode (fixes #396) Fix false local repo detection when users run `uv init` before installation. Changed detection from naive file existence check to content validation: - For pyproject.toml: check for `name = "vocalinux"` - For setup.py: check for "vocalinux" string - Falls through to remote clone mode if neither matches Fixes #396 * fix(recognition): preserve final speech on stop (fixes #401) Replace hard-coded stop tail trim with configurable stop_sound_guard_ms (default 200ms) so quick key release no longer clips the final spoken word in push-to-talk mode. Co-authored-by: Fakhareddine <fe.sabbari@claranet.com> * fix(ibus): handle engine instance destruction on layout switch (fixes #388, closes #389) Override do_destroy() to clear _active_instance when IBus destroys the engine on keyboard layout switch (Super+Space). Add retry logic in inject_text() — on NO_ENGINE, re-activates via switch_engine() and retries once to recover cleanly. Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com> * chore: ignore opentree worktree dir and config (#412) .opentree/ contains git worktrees created by the opentree CLI tool (https://github.com/axelgar/opentree). These are local developer artifacts and should not be tracked. opentree.toml is the per-repo opentree config - also local/personal, not something contributors need to share. * test(ibus): fix do_destroy CI failure (issubclass TypeError without IBus) (#410) * test(ibus): fix do_destroy super() test failing in CI without IBus installed Patch builtins.super instead of relying on mock_ibus.Engine.do_destroy, which fails with issubclass() TypeError when VocalinuxEngine is built with object as its base (IBus unavailable in CI). * test(ibus): fix MagicMock.__new__ TypeError on Python < 3.13 VocalinuxEngine inherits from MagicMock at import time (IBus is mocked via sys.modules at test-file level). Calling VocalinuxEngine.__new__(VocalinuxEngine) delegates to MagicMock.__new__ which runs issubclass(cls, AsyncMockMixin). On Python < 3.13 that raises TypeError when the module-cached class is a MagicMock spec object rather than a real type. Fix: introduce _make_engine() helper that allocates via a thin concrete subclass whose __new__ calls object.__new__ directly, bypassing the MagicMock allocation path entirely. Verified across all three TestVocalinuxEngineDestroy test methods. * test(ibus): use unbound do_destroy call to avoid MagicMock interference The previous fix (thin _ConcreteEngine subclass) still failed because VocalinuxEngine inherits MagicMock.__getattribute__, which intercepts .do_destroy on instances and returns a mock attribute rather than the real method - regardless of Python version. Call VocalinuxEngine.do_destroy(engine) directly with a plain object() as the self argument instead. do_destroy only uses self for identity comparison (is VocalinuxEngine._active_instance), so any object works. This bypasses MagicMock's MRO entirely. * test(ibus): reload module and instantiate real engine class for do_destroy tests * refactor(ibus): extract destroy handler for stable cross-version tests * test(ibus): cover destroy helper branches and do_destroy delegation * test(ibus): stabilize do_destroy CI behavior across environments Avoid brittle super() mocking in no-IBus environments and gate the IBus-only parent destroy path test behind runtime capability checks to prevent false CI failures on Python matrix runners. * test(ibus): remove flaky do_destroy class-path assertions Drop brittle class-method destroy tests that fail under mocked GI/IBus on older Python runners and keep deterministic helper-level coverage for destroy behavior. * fix(text-injection): recover from IBus runtime failures (#411) * fix(ibus): retry and recover from transient socket failures Harden IBus injection against connection-refused, timeout, and engine process races so dictation is more resilient in real-world desktop sessions. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(text-injection): fallback to non-IBus backend on runtime IBus failures Allow dictation to continue when IBus runtime injection fails by automatically switching to xdotool/wtype/ydotool fallbacks instead of returning an immediate failure. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(ibus): preserve live socket injection during recovery Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * test(coverage): add runtime fallback and ibus retry coverage Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(ibus): probe engine readiness at startup and harden NO_ENGINE retries (#391) * fix(ibus): wait for engine readiness before first injection Add a startup readiness probe that pings the IBus engine after activation so first-use dictation no longer races do_enable(). Also switch NO_ENGINE retry behavior to exponential backoff and cover the new flow in tests. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(ibus): cover readiness probe edge cases Add focused tests for the new startup readiness path, including direct PING success, retry exhaustion, unexpected responses, and engine re-activation before injection. This raises coverage on vocalinux.text_injection.ibus_engine from 68% to 73% in the targeted suite. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * fix(text-injection): avoid blocking IBus and clipboard fallbacks Warm IBus in the background so startup no longer stalls while readiness probing, and make clipboard helpers fail fast with per-session health caching so non-ASCII ydotool fallback does not lock dictation. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * test(ibus): update retry tests for merged NO_ENGINE recovery logic --------- Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> * chore(deps-dev): bump postcss from 8.4.39 to 8.5.10 in /web Bump postcss dev dependency (CSS processing). No runtime impact. * fix(logging): clean up runtime log noise and cache hardware detection Clean up duplicated code artifacts from recent merges and cache hardware detection to eliminate ~1.3s UI lag when opening settings dialog. * test: cover recognition internals and suppress desktop notifications (#414) * test(recognition): cover _perform_recognition and _enqueue_audio_segment The previous commit demoted logger.info('DEBUG: ...') to logger.debug(...) and removed a dead loop body in _perform_recognition. Codecov flagged 12 uncovered lines — all inside _perform_recognition and _enqueue_audio_segment, which existing tests always mocked out. Add direct (non-mocked) tests for both methods: - _enqueue_audio_segment: * empty buffer → early return * normal buffer → enqueues correctly * full queue → drops oldest and retries - _perform_recognition: * processes a segment then exits on None signal * drains remaining items on None signal * exits cleanly when idle with empty queue * continues looping on queue timeout while recording These tests exercise the actual threading/queue logic and cover the logger.debug / logger.warning calls as a side effect. * test(conftest): suppress real desktop notifications during test runs _show_notification shells out to notify-send, which pops up real system notifications when tests exercise the model-not-ready path. Add an autouse fixture that patches _show_notification for all tests EXCEPT those whose name contains 'show_notification' (so direct tests of the helper still verify subprocess.Popen calls). * chore(gitignore): ignore .sisyphus directory * fix(install): improve openSUSE dependency handling with fallbacks (#418) The suse case in install_system_dependencies() previously installed all packages blindly, causing the entire install to fail if a single package was unavailable (e.g., glslang on some openSUSE repos). Changes: - Check each package individually with rpm -q before installing - Add graceful fallback for glslang -> glslang-devel - Add graceful fallback for libappindicator-gtk3 -> typelib-1_0-AppIndicator3-0_1 -> libayatana-appindicator3-1 - Continue installation with warning instead of hard failure Fixes #407 * fix(remote_api): re-init engine on resume from suspend Add a remote_api branch to reinitialize_after_resume mirroring the dispatch chain in __init__, so the engine is recreated after a suspend/resume cycle. Null self._http_session alongside the existing self.model and self.recognizer resets so a stale pooled-connection session is not reused after resume. * perf(remote_api): pool HTTP connections via requests.Session Introduces a manager-owned requests.Session (_http_session) so all remote_api engine HTTP traffic reuses a single connection pool instead of opening a new connection for every request. Lifecycle: - __init__: self._http_session = None (attribute always present) - _init_remote_api: close prior session if it exists, then create a new requests.Session(); use session.get for the connection test. Prevents connection leaks on back-to-back inits (re-init guard). - _try_openai_api / _try_whispercpp_server_api: replaced requests.post with self._http_session.post; local 'import requests' retained for the requests.exceptions.ConnectionError catch. - reconfigure: captures old engine before self.engine is updated; closes and nulls _http_session when switching away from remote_api. - reinitialize_after_resume: nulls _http_session before _init_remote_api recreates it. Tests: added TestHTTPSession class (4 cases) in tests/test_recognition_manager_remote_api.py; all 40 tests pass. Updated _setup_requests_get_*/post_* helpers to configure the session instance (mock_requests.Session.return_value) instead of the module-level mock, keeping all 36 pre-existing tests green. * docs: add remote HTTP transcription setup guide * fix(install): avoid asking for remote server URL twice run_interactive_install collects the URL upfront in Step 3, but install_python_package re-declared REMOTE_API_URL as local and prompted again, ignoring the wizard value. Inherit the wizard's value instead. * ui(settings): hide remote API behind an Advanced tab Addresses upstream PR #335 review feedback from @jatinkrmalik that remote API is not plug-and-play and should not be a peer of the local engines for casual users. - Remove 'Remote API' from the Speech Engine tab's engine combo; the combo now shows local engines only (vosk / whisper / whisper_cpp). - Add a new 'Advanced' notebook tab with a 'Remote Server' group containing a 'Use remote server' switch, the existing URL/Key/ Endpoint fields, and the Test Connection button + status label. - The switch overrides the local engine selection. get_selected_settings returns engine='remote_api' iff the switch is on, otherwise the combo selection. URL/Key/Endpoint persist independently of the toggle state. - _load_and_apply_settings: if config has engine='remote_api', the switch is turned on and the combo is set to a sensible local fallback (whisper_cpp, or first available local engine) so toggling off has somewhere to land. - _update_engine_specific_ui no longer special-cases the combo for remote_api (the combo never has it); it hides the model row, info card and legend whenever the remote switch is on. - Update HTTP_REMOTE.md to reflect the new tab location. * ui(remote-api): mark remote API as advanced and default toggle off - install.sh: relabel option 4 as 'REMOTE API (ADVANCED)' to signal it as a non-default, advanced engine choice. - settings_dialog.py: explicitly initialise the 'Use remote server' switch to inactive at widget creation, making the default state unambiguous (saved-config load logic still toggles it on for existing remote_api installs). * fix(remote-api): preserve trailing newline from transcription responses The remote-API transcription path stripped all whitespace from the upstream response before injection, silently discarding a trailing newline that some pipelines (post-processing proxies, fixup layers) emit to signal "submit" / Enter. Whisper itself doesn't emit trailing newlines, so for the common case this is a no-op; for users with a post-processing layer it stops their submit signal from being eaten. _filter_non_speech now uses a whitespace-stripped probe for its non-speech pattern checks but returns probe + "\n" when the original input ended with "\n" or "\r" (CRLF normalises to LF). The redundant .strip() at the call site is removed so the function receives the raw API text. Existing filter behaviour (BLANK_AUDIO, music notes, low speech-content ratio, etc.) is unchanged. Four new unit tests cover the trailing-newline cases. * test(remote-api): cover transcribe exception handler and unknown-engine reinit branch Adds two unit tests buttoning up previously-uncovered defensive branches: - _transcribe_with_remote_api swallows unexpected exceptions and returns "" — verified by raising RuntimeError from _try_whispercpp_server_api. - reinitialize_after_resume short-circuits without dispatching when the engine attribute is unknown — verified by mutating engine to a bogus value and asserting no _init_* method is called. * feat(settings): add Advanced tab with whisper.cpp anti-hallucination parameters (#415) * feat(settings): add Advanced tab with whisper.cpp anti-hallucination parameters Add a new Advanced settings tab that exposes whisper.cpp decoding parameters to help users reduce hallucinations and tune recognition behavior. Parameters exposed: - No Timestamps (default: on) - Suppress Non-Speech Tokens (default: on) - No Context (default: on) - Initial Prompt (default: empty) - Temperature (default: 0.0) - Temperature Increment (default: -1.0, disables fallback) - Entropy Threshold (default: 2.4) - Logprob Threshold (default: -1.0) - No-Speech Threshold (default: 0.6) All parameters are saved under the advanced config section and are only active when the whisper.cpp engine is selected. The UI grays out these settings and shows an explanatory note when another engine is active. Changes: - config_manager.py: add whisper.cpp keys to DEFAULT_CONFIG[advanced] - recognition_manager.py: read params from kwargs/config, pass to Model() - main.py: pass advanced settings from config to SpeechRecognitionManager - settings_dialog.py: new Advanced tab with switches, spin buttons, entry - tests: update test_main.py assertions, add whispercpp_advanced_defaults and whispercpp_advanced_persistence tests Fixes #363 * fix(whispercpp): gracefully filter unsupported params for pywhispercpp 1.4.1 pywhispercpp 1.4.1 does not expose no_timestamps or suppress_non_speech_tokens to Python even though they exist in the underlying whisper.cpp C struct. This causes AttributeError on startup. Add _load_model_with_compatible_params() which catches AttributeError, identifies the unsupported param from the error message, removes it, and retries until the model loads successfully or a non-AttributeError occurs. Also refactor _build_whispercpp_model_kwargs() from _load_whispercpp_model() for testability. Fixes local startup crash reported during PR testing. * fix(whispercpp): persist advanced settings correctly * chore(config): add advanced power-user default Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * feat(settings): gate advanced controls behind power-user opt-in Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) * fix(whispercpp): prefilter unsupported native params * fix(settings): polish advanced whispercpp controls * fix(installer): reuse existing whispercpp builds (#421) * docs: clarify PyPI installation requirements (#423) * chore(deps): bump next (#429) Bumps the npm_and_yarn group with 1 update in the /web directory: [next](https://github.com/vercel/next.js). Updates `next` from 15.5.15 to 15.5.18 - [Release notes](https://github.com/vercel/next.js/releases) - [Changelog](https://github.com/vercel/next.js/blob/canary/release.js) - [Commits](https://github.com/vercel/next.js/compare/v15.5.15...v15.5.18) --- updated-dependencies: - dependency-name: next dependency-version: 15.5.18 dependency-type: direct:production dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(installer): repair pywhispercpp library loading (#433) * fix: play stop sound immediately on release (#426) * fix(recognition): play stop sound after audio thread joins to prevent capture on short recordings (#436) Moves play_stop_sound() to after audio_thread.join() but before the final buffer processing. This closes the race where the audio thread's in-flight chunk could capture the stop cue for short (≤ ~192 ms) recordings, while still feeling immediate compared to the original placement after all recognition cleanup. * fix(install): correct openSUSE Tumbleweed dependencies (#420) * fix(install): correct openSUSE Tumbleweed dependencies * style: format openSUSE installer changes --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> * fix(installer): harden Debian compatibility layer (#437) Add missing pywhispercpp build prerequisites (libssl-dev, autoconf, automake, libtool, patchelf) to both Debian and Ubuntu apt package lists. These are not pulled in transitively on clean Debian installs and cause CMake bootstrap failures during source compilation. Add runtime smoke test for pywhispercpp to verify_installation() so "installs but does not run" failures surface at install time with actionable diagnostic steps instead of silently at first launch. Treat Debian as a first-class target — remove the "primarily designed for Ubuntu" warning for Debian systems. Remain conservative for non-Debian, non-Ubuntu distros. Add DEBIAN_FRONTEND=noninteractive to all apt install commands to prevent interactive debconf prompts from blocking unattended installs. Improve ydotool failure messaging on Debian with compile-from-source instructions since ydotool is not packaged in Debian standard repos. Add Debian-specific troubleshooting section to DISTRO_COMPATIBILITY.md covering build deps, ydotool compilation, scoped source builds, and libwhisper.so resolution diagnostics. References: https://medium.com/@cslev/talking-to-my-linux-box-without-talking-to-the-cloud-vocalinux-on-debian-without-the-tears-10bf053ea21b * fix(install): refresh ldconfig after openSUSE typelib install, clarify python3XY placeholder convention (#438) - Add 'sudo ldconfig' after zypper install in suse_install_appindicator_runtime() to ensure GI typelib cache is refreshed so newly installed typelib packages are immediately discoverable by GObject introspection - Add section-level comment in distro-package-map.yaml explaining that python3XY is a placeholder convention resolved at install time by suse_python_package_prefix() / suse_python_package_candidates() Follow-up polish from review of #420. * fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode (#439) * fix(whispercpp): reduce CPU threads and ensure GPU backend builds in dev mode - Reduce default n_threads from cpu_count() (16) to max 8 (CPU) / max 4 (GPU) - Add runtime pywhispercpp GPU capability detection (_detect_pywhispercpp_gpu_backend) - Make n_threads configurable via whispercpp_n_threads config option - Fix install.sh regression: GPU source build was skipped in --dev mode - Extract GPU install logic into install_whispercpp_with_gpu_support() - Add is_pywhispercpp_gpu_capable() to detect GPU libs in installed pywhispercpp - Force rebuild pywhispercpp when GPU is detected but lacks GPU support Fixes CPU spiking to 100% on GPU systems due to excessive thread count and ensures dev installs get GPU acceleration. * fix(audio): skip output-only HDMI devices when selecting input device Add _resolve_valid_input_device() that filters devices by maxInputChannels > 0, falling back preferred → default → first valid. Previously, HDMI output devices (0 input channels) could be selected as the default audio input, causing 'Invalid number of channels' errors. Now both _record_audio() and _attempt_audio_reconnection() validate the device has input channels before probing or opening the stream. * fix(whispercpp): treat n_threads=0 as auto-detect, not literal zero Config default whispercpp_n_threads=0 was being passed directly to whisper.cpp as n_threads=0, causing 'cannot create std::vector larger than max_size()' transcription errors. Now 0 and negative values are treated as auto-detect, falling through to the smart thread count logic (max 4 with GPU, max 8 on CPU). * test: add whispercpp_n_threads to mock assertions and config defaults Update test_main.py mock to include the new whispercpp_n_threads=0 kwarg and test_config_manager.py to assert the default config value. * fix(audio): harden device enumeration against test mocks The new HDMI-filter helper and GPU-backend detector crashed unit tests that pass MagicMock() for the PyAudio/pathlib surfaces: - _find_pywhispercpp_shared_library_dirs hit TypeError when tests monkey-patch os.stat (Path.is_dir internally calls S_ISDIR on a non-int st_mode). Widen the catch to OSError/TypeError/ValueError so unconfigured stat results just skip the candidate dir. - _resolve_valid_input_device crashed with 'MagicMock > int' on audio.get_device_count() / info.get('maxInputChannels'). Coerce the count, treat zero/unknown as 'use system default', and include non-dict info objects rather than filtering everything out. No production behavior changes: real PyAudio always returns int counts and dict device info, so the HDMI filter still works as designed. * test(recognition): cover new HDMI filter and GPU backend detector - TestResolveValidInputDevice: 9 cases for the HDMI-filter resolver (preferred passthrough, HDMI fallback, no-default fallback, no-input None return, and three mock-tolerance escape hatches). - TestDetectPywhispercppGpuBackend: 4 cases for runtime GPU library detection (cpu/vulkan/cuda/empty dirs). - TestFindPywhispercppSharedLibraryDirsResilience: proves the widened exception catch survives a TypeError from pathlib.Path.is_dir. * test(text-injection): wait for IBus init thread to avoid race on slow runners The IBus environment promotion runs in a background daemon thread spawned by TextInjector.__init__. The two TestIBusRuntimeFallback cases asserted on injector.environment immediately after construction, which raced the thread on Python 3.10 GHA runners and produced intermittent failures. Join the init thread (with a generous timeout) before asserting, so the test exercises the post-warmup state deterministically. * chore(release): prepare v0.11.0-beta (#440) - Bump version from 0.10.2-beta to 0.11.0-beta - Update all version references across codebase - Add What's New section for v0.11.0-beta in README and docs/UPDATE.md - Add v0.11.0-beta entry to website changelog - Update SECURITY.md supported versions to 0.11.x - Update website schema, badge, and package version - Update RELEASE_PROCESS.md version history table * fix(startup): allow launch without pynput backend (#448) * feat(vad): add Silero VAD and drop silence-only buffers (#447) * feat(vad): replace amplitude-based VAD with Silero neural network Replace the simple amplitude threshold VAD with Silero VAD, a lightweight neural network model that classifies audio as speech/non-speech. This addresses the root cause of #302 (silence not stopping recording) and - Add silero_vad.py wrapper using ONNX Runtime with bundled Silero v5 model - Accumulate 512-sample chunks with 64-sample context window per official API - Map existing vad_sensitivity (1-5) to Silero thresholds (0.8-0.3) - Use max() across sub-chunks to avoid missing speech onsets - Fall back to amplitude VAD when onnxruntime is unavailable - Add onnxruntime as optional dependency (pip install vocalinux[vad]) Closes #344 * address review: add tests, LICENSE, logging, validation fixes - Add unit tests for SileroVAD (process, reset, fallback, sensitivity mapping) - Add MIT LICENSE for bundled Silero VAD model in data/ - Log which VAD backend is active on init - Validate chunk length in SileroVAD.process() - Skip is_speech check until a full Silero chunk is processed - Add thread safety note to SileroVAD docstring - Document resampling rate effect on VAD resolution - Comment explaining why load_silero_vad() catches broad Exception * address review: bundle license, document [vad] extra, surface backend in UI - Pack data/LICENSE into the wheel (pyproject package-data) so attribution travels with the model - README: short note on enabling neural VAD via vocalinux[vad] - Settings: VAD Sensitivity row subtitle shows the active backend; if onnxruntime is missing, an info-row beneath the group spells out the install command and the restart requirement - silero_vad.is_silero_available() probes onnxruntime + model presence w/o instantiating an InferenceSession - CI installs .[dev,vad] so Silero paths get exercised - Integration test for _record_audio Silero branch (mocked pyaudio + scripted SileroVAD), covering silence flush, speech reset, and sensitivity threshold mapping at sens=1 vs sens=5 - Tests no longer skip when other modules globally mock numpy: pop the top-level numpy ref so a fresh import gives the real package, and swap sys.modules["numpy"] inside the _record_audio call so the lazy import inside the function picks up real numpy too * fix(vad): drop silence-only recording buffers * test(vad): align stop buffer expectations * test(vad): preserve real numpy module * fix(install): install vad support opportunistically * test(vad): cover recognition manager vad edges --------- Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com> * fix(web): clarify speech demo browser support (#449) * fix black format check --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: jay <sevenjay@users.noreply.github.com> Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com> Co-authored-by: Fakhareddine <fe.sabbari@claranet.com> Co-authored-by: Sbenazar <127324689+Sbenazar@users.noreply.github.com> Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: parko636 <alex@alexparkinson.net> Co-authored-by: juanfradb <94815906+juanfradb@users.noreply.github.com> Co-authored-by: Pep Locanto <peplocanto@gmail.com>
3594 lines
144 KiB
Bash
Executable File
3594 lines
144 KiB
Bash
Executable File
#!/bin/bash
|
||
# Vocalinux Installer
|
||
# This script installs the Vocalinux application and its dependencies
|
||
|
||
set -e # Exit on error
|
||
|
||
# Function to display colored output
|
||
print_info() {
|
||
echo -e "\e[1;34m[INFO]\e[0m $1"
|
||
}
|
||
|
||
print_success() {
|
||
echo -e "\e[1;32m[SUCCESS]\e[0m $1"
|
||
}
|
||
|
||
print_error() {
|
||
echo -e "\e[1;31m[ERROR]\e[0m $1"
|
||
}
|
||
|
||
print_warning() {
|
||
echo -e "\e[1;33m[WARNING]\e[0m $1"
|
||
}
|
||
|
||
command_exists() {
|
||
command -v "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
get_vocalinux_pids() {
|
||
pgrep -f "vocalinux" 2>/dev/null | while read -r pid; do
|
||
[ -z "$pid" ] && continue
|
||
|
||
if [ "$pid" = "$$" ] || [ "$pid" = "$PPID" ]; then
|
||
continue
|
||
fi
|
||
|
||
local stat
|
||
stat=$(ps -o stat= -p "$pid" 2>/dev/null | awk '{print $1}')
|
||
if [[ "$stat" == Z* ]]; then
|
||
continue
|
||
fi
|
||
|
||
local cmd
|
||
cmd=$(ps -o args= -p "$pid" 2>/dev/null)
|
||
if [[ "$cmd" == *"install.sh"* ]] || [[ "$cmd" == *"uninstall.sh"* ]]; then
|
||
continue
|
||
fi
|
||
|
||
echo "$pid"
|
||
done
|
||
}
|
||
|
||
check_running_processes() {
|
||
local PIDS
|
||
PIDS=$(get_vocalinux_pids || true)
|
||
|
||
if [ -n "$PIDS" ]; then
|
||
print_warning "Found running Vocalinux process(es): $PIDS"
|
||
echo ""
|
||
|
||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||
print_info "Non-interactive mode: stopping Vocalinux automatically..."
|
||
else
|
||
read -p "Vocalinux must be stopped before installation. Kill running process(es)? (Y/n) " -n 1 -r
|
||
echo
|
||
if [[ $REPLY =~ ^[Nn]$ ]]; then
|
||
print_error "Cannot proceed with installation while Vocalinux is running."
|
||
print_info "Please stop Vocalinux manually and run the installer again."
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
print_info "Stopping Vocalinux..."
|
||
echo "$PIDS" | xargs -r kill -TERM 2>/dev/null || true
|
||
sleep 2
|
||
|
||
local REMAINING_PIDS
|
||
REMAINING_PIDS=$(get_vocalinux_pids || true)
|
||
|
||
if [ -n "$REMAINING_PIDS" ]; then
|
||
print_warning "Some processes still running, forcing termination..."
|
||
echo "$REMAINING_PIDS" | xargs -r kill -KILL 2>/dev/null || true
|
||
sleep 1
|
||
fi
|
||
|
||
local FINAL_PIDS
|
||
FINAL_PIDS=$(get_vocalinux_pids || true)
|
||
if [ -n "$FINAL_PIDS" ]; then
|
||
print_error "Could not terminate all Vocalinux processes: $FINAL_PIDS"
|
||
print_error "Please manually kill these processes and run the installer again."
|
||
exit 1
|
||
else
|
||
print_success "All Vocalinux processes stopped"
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# Parse command line arguments
|
||
INSTALL_MODE="user"
|
||
RUN_TESTS="no"
|
||
DEV_MODE="no"
|
||
VENV_DIR="venv"
|
||
SKIP_MODELS="no"
|
||
SKIP_SYSTEM_DEPS="no"
|
||
WITH_WHISPER="no"
|
||
WHISPER_CPU="no"
|
||
NO_WHISPER_EXPLICIT="no"
|
||
NON_INTERACTIVE="no"
|
||
INTERACTIVE_MODE="yes" # Default to interactive mode
|
||
AUTO_MODE="no"
|
||
REBUILD_WHISPERCPP="ask"
|
||
HAS_NVIDIA_GPU="unknown"
|
||
GPU_NAME=""
|
||
GPU_MEMORY=""
|
||
HAS_VULKAN="no"
|
||
VULKAN_DEVICE=""
|
||
|
||
# Detect if running non-interactively (e.g., via curl | bash)
|
||
# If stdin is a pipe but /dev/tty exists, redirect stdin so user input works normally.
|
||
# If no terminal is available at all (headless/CI), fall back to automatic mode.
|
||
if [ ! -t 0 ]; then
|
||
if [ -e /dev/tty ] && [ -r /dev/tty ]; then
|
||
if { true < /dev/tty; } 2>/dev/null; then
|
||
exec < /dev/tty
|
||
INTERACTIVE_MODE="ask"
|
||
else
|
||
AUTO_MODE="yes"
|
||
INTERACTIVE_MODE="no"
|
||
NON_INTERACTIVE="yes"
|
||
fi
|
||
else
|
||
AUTO_MODE="yes"
|
||
INTERACTIVE_MODE="no"
|
||
NON_INTERACTIVE="yes"
|
||
fi
|
||
fi
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case $1 in
|
||
--dev)
|
||
DEV_MODE="yes"
|
||
shift
|
||
;;
|
||
--test)
|
||
RUN_TESTS="yes"
|
||
shift
|
||
;;
|
||
--venv-dir=*)
|
||
VENV_DIR="${1#*=}"
|
||
shift
|
||
;;
|
||
--skip-models)
|
||
SKIP_MODELS="yes"
|
||
shift
|
||
;;
|
||
--skip-system-deps)
|
||
SKIP_SYSTEM_DEPS="yes"
|
||
shift
|
||
;;
|
||
--rebuild-whispercpp)
|
||
REBUILD_WHISPERCPP="yes"
|
||
shift
|
||
;;
|
||
--no-rebuild-whispercpp)
|
||
REBUILD_WHISPERCPP="no"
|
||
shift
|
||
;;
|
||
--engine=*)
|
||
SELECTED_ENGINE="${1#*=}"
|
||
shift
|
||
;;
|
||
--interactive|-i)
|
||
INTERACTIVE_MODE="yes"
|
||
shift
|
||
;;
|
||
--tag=*)
|
||
INSTALL_TAG="${1#*=}"
|
||
shift
|
||
;;
|
||
--auto)
|
||
AUTO_MODE="yes"
|
||
INTERACTIVE_MODE="no"
|
||
NON_INTERACTIVE="yes"
|
||
shift
|
||
;;
|
||
--help)
|
||
echo "Vocalinux Installer"
|
||
echo ""
|
||
echo "Usage: $0 [options]"
|
||
echo ""
|
||
echo "Installation Modes:"
|
||
echo " (no flags) Interactive mode - guided setup with recommendations"
|
||
echo " --auto Automatic mode - install with defaults (whisper.cpp)"
|
||
echo " --auto --engine=whisper Auto mode with specific engine"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --interactive, -i Force interactive mode (default)"
|
||
echo " --auto Non-interactive automatic installation"
|
||
echo " --engine=NAME Speech engine: whisper_cpp (default), whisper, vosk, remote_api"
|
||
echo " --dev Install in development mode with all dev dependencies"
|
||
echo " --test Run tests after installation"
|
||
echo " --venv-dir=PATH Specify custom virtual environment directory"
|
||
echo " --skip-models Skip downloading speech models during installation"
|
||
echo " --skip-system-deps"
|
||
echo " Skip package-manager dependency installation (advanced)"
|
||
echo " --rebuild-whispercpp Rebuild/reinstall pywhispercpp even if already installed"
|
||
echo " --no-rebuild-whispercpp Reuse existing pywhispercpp when present (auto-mode default)"
|
||
echo " --tag=TAG Install specific release tag (default: latest release)"
|
||
echo " --help Show this help message"
|
||
echo ""
|
||
echo "Examples:"
|
||
echo " $0 # Interactive mode (recommended)"
|
||
echo " $0 --auto # Auto-install with whisper.cpp"
|
||
echo " $0 --auto --engine=vosk # Auto-install VOSK only"
|
||
echo " $0 --dev --test # Dev mode with tests"
|
||
exit 0
|
||
;;
|
||
*)
|
||
print_error "Unknown option: $1"
|
||
echo "Use --help to see available options"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# If dev mode is enabled, automatically run tests
|
||
if [[ "$DEV_MODE" == "yes" ]]; then
|
||
RUN_TESTS="yes"
|
||
fi
|
||
|
||
# Display ASCII art banner
|
||
cat << "EOF"
|
||
|
||
▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▄▖ ▗▖ ▗▄▄▄▖▗▖ ▗▖▗▖ ▗▖▗▖ ▗▖
|
||
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ ▐▌▐▌ █ ▐▛▚▖▐▌▐▌ ▐▌ ▝▚▞▘
|
||
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▛▀▜▌▐▌ █ ▐▌ ▝▜▌▐▌ ▐▌ ▐▌
|
||
▝▚▞▘ ▝▚▄▞▘▝▚▄▄▖▐▌ ▐▌▐▙▄▄▖▗▄█▄▖▐▌ ▐▌▝▚▄▞▘▗▞▘▝▚▖
|
||
|
||
Voice Dictation for Linux
|
||
|
||
EOF
|
||
|
||
print_info "Vocalinux Installer"
|
||
print_info "=============================="
|
||
echo ""
|
||
|
||
check_running_processes
|
||
|
||
resolve_install_tag() {
|
||
if [ -n "$INSTALL_TAG" ]; then
|
||
return
|
||
fi
|
||
if command_exists curl; then
|
||
local latest
|
||
latest=$(curl -fsSL --connect-timeout 5 \
|
||
"https://api.github.com/repos/jatinkrmalik/vocalinux/releases/latest" \
|
||
2>/dev/null | grep '"tag_name"' | head -1 | cut -d'"' -f4)
|
||
if [ -n "$latest" ]; then
|
||
INSTALL_TAG="$latest"
|
||
return
|
||
fi
|
||
fi
|
||
INSTALL_TAG="v0.10.1-beta"
|
||
}
|
||
|
||
resolve_install_tag
|
||
|
||
# Check if running from within the vocalinux repo or remotely (via curl)
|
||
REPO_URL="https://github.com/jatinkrmalik/vocalinux.git"
|
||
INSTALL_DIR=""
|
||
CLEANUP_ON_EXIT="no"
|
||
|
||
# Function to check and install git if needed
|
||
ensure_git_installed() {
|
||
if command -v git >/dev/null 2>&1; then
|
||
return 0
|
||
fi
|
||
|
||
print_warning "git is not installed. Attempting to install git..."
|
||
|
||
# Detect distribution for package manager selection
|
||
local DISTRO_FAMILY="unknown"
|
||
if [ -f /etc/os-release ]; then
|
||
. /etc/os-release
|
||
if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* || "$ID" == "pop" || "$ID" == "linuxmint" || "$ID" == "elementary" || "$ID" == "zorin" ]]; then
|
||
DISTRO_FAMILY="ubuntu"
|
||
elif [[ "$ID" == "debian" || "$ID_LIKE" == *"debian"* ]]; then
|
||
DISTRO_FAMILY="debian"
|
||
elif [[ "$ID" == "fedora" || "$ID_LIKE" == *"fedora"* || "$ID" == "rhel" || "$ID" == "centos" || "$ID" == "rocky" || "$ID" == "almalinux" ]]; then
|
||
DISTRO_FAMILY="fedora"
|
||
elif [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* || "$ID" == "manjaro" || "$ID" == "endeavouros" ]]; then
|
||
DISTRO_FAMILY="arch"
|
||
elif [[ "$ID" == "opensuse" || "$ID_LIKE" == *"suse"* ]]; then
|
||
DISTRO_FAMILY="suse"
|
||
elif [[ "$ID" == "gentoo" ]]; then
|
||
DISTRO_FAMILY="gentoo"
|
||
elif [[ "$ID" == "alpine" ]]; then
|
||
DISTRO_FAMILY="alpine"
|
||
elif [[ "$ID" == "void" ]]; then
|
||
DISTRO_FAMILY="void"
|
||
elif [[ "$ID" == "solus" ]]; then
|
||
DISTRO_FAMILY="solus"
|
||
elif [[ "$ID" == "mageia" ]]; then
|
||
DISTRO_FAMILY="mageia"
|
||
fi
|
||
fi
|
||
|
||
case "$DISTRO_FAMILY" in
|
||
ubuntu|debian)
|
||
sudo apt update && sudo apt install -y git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " Ubuntu/Debian: sudo apt install git"
|
||
exit 1
|
||
}
|
||
;;
|
||
fedora)
|
||
sudo dnf install -y git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " Fedora: sudo dnf install git"
|
||
exit 1
|
||
}
|
||
;;
|
||
arch)
|
||
sudo pacman -S --noconfirm git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " Arch: sudo pacman -S git"
|
||
exit 1
|
||
}
|
||
;;
|
||
suse)
|
||
sudo zypper install -y git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " openSUSE: sudo zypper install git"
|
||
exit 1
|
||
}
|
||
;;
|
||
gentoo)
|
||
sudo emerge git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " Gentoo: sudo emerge git"
|
||
exit 1
|
||
}
|
||
;;
|
||
alpine)
|
||
sudo apk add git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " Alpine: sudo apk add git"
|
||
exit 1
|
||
}
|
||
;;
|
||
void)
|
||
sudo xbps-install -Sy git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " Void: sudo xbps-install -Sy git"
|
||
exit 1
|
||
}
|
||
;;
|
||
solus)
|
||
sudo eopkg install git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
print_error " Solus: sudo eopkg install git"
|
||
exit 1
|
||
}
|
||
;;
|
||
mageia)
|
||
if command -v dnf >/dev/null 2>&1; then
|
||
sudo dnf install -y git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
exit 1
|
||
}
|
||
else
|
||
sudo urpmi --force git || {
|
||
print_error "Failed to install git. Please install git manually and run the installer again."
|
||
exit 1
|
||
}
|
||
fi
|
||
;;
|
||
*)
|
||
print_error "git is not installed and could not auto-detect your distribution."
|
||
print_error "Please install git manually and run the installer again:"
|
||
print_error " Ubuntu/Debian: sudo apt install git"
|
||
print_error " Fedora/RHEL: sudo dnf install git"
|
||
print_error " Arch: sudo pacman -S git"
|
||
print_error " openSUSE: sudo zypper install git"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
print_success "git installed successfully!"
|
||
}
|
||
|
||
# Check if running from within the vocalinux repository
|
||
# Validate that pyproject.toml/setup.py actually belongs to vocalinux before entering local repo mode
|
||
IS_VOCALINUX_LOCAL=false
|
||
if [ -f "pyproject.toml" ] && grep -q 'name = "vocalinux"' "pyproject.toml" 2>/dev/null; then
|
||
IS_VOCALINUX_LOCAL=true
|
||
elif [ -f "setup.py" ] && grep -q "vocalinux" "setup.py" 2>/dev/null; then
|
||
IS_VOCALINUX_LOCAL=true
|
||
fi
|
||
|
||
if [ "$IS_VOCALINUX_LOCAL" = true ]; then
|
||
# Running from within the vocalinux repo
|
||
INSTALL_DIR="$(pwd)"
|
||
print_info "Running from local repository: $INSTALL_DIR"
|
||
# Convert VENV_DIR to absolute path for wrapper scripts
|
||
VENV_DIR="$INSTALL_DIR/$VENV_DIR"
|
||
else
|
||
# Running remotely (e.g., via curl | bash)
|
||
print_info "Installing Vocalinux version: ${INSTALL_TAG}"
|
||
|
||
# Ensure git is installed before attempting to clone
|
||
ensure_git_installed
|
||
|
||
INSTALL_DIR="$HOME/.local/share/vocalinux-install"
|
||
mkdir -p "$INSTALL_DIR"
|
||
|
||
if [ -d "$INSTALL_DIR/.git" ]; then
|
||
print_info "Updating existing clone..."
|
||
cd "$INSTALL_DIR"
|
||
git fetch origin tag "$INSTALL_TAG"
|
||
git reset --hard "$INSTALL_TAG"
|
||
else
|
||
rm -rf "$INSTALL_DIR"
|
||
git clone --depth 1 --branch "$INSTALL_TAG" "$REPO_URL" "$INSTALL_DIR" || {
|
||
print_error "Failed to clone Vocalinux repository"
|
||
exit 1
|
||
}
|
||
cd "$INSTALL_DIR"
|
||
fi
|
||
CLEANUP_ON_EXIT="yes"
|
||
print_info "Repository cloned to: $INSTALL_DIR"
|
||
|
||
# When running remotely, install venv to user's home directory
|
||
VENV_DIR="$HOME/.local/share/vocalinux/venv"
|
||
fi
|
||
|
||
# Change to install directory
|
||
cd "$INSTALL_DIR"
|
||
|
||
print_info "Using virtual environment: $VENV_DIR"
|
||
[[ "$DEV_MODE" == "yes" ]] && print_info "Installing in development mode"
|
||
[[ "$RUN_TESTS" == "yes" ]] && print_info "Tests will be run after installation"
|
||
echo ""
|
||
|
||
# Check if running as root
|
||
if [ "$EUID" -eq 0 ]; then
|
||
print_error "Please do not run this script as root or with sudo."
|
||
exit 1
|
||
fi
|
||
|
||
# Detect Linux distribution and version
|
||
detect_distro() {
|
||
if [ -f /etc/os-release ]; then
|
||
. /etc/os-release
|
||
DISTRO_NAME="$NAME"
|
||
DISTRO_ID="$ID"
|
||
DISTRO_VERSION="$VERSION_ID"
|
||
DISTRO_FAMILY="unknown"
|
||
|
||
# Determine distribution family
|
||
if [[ "$ID" == "ubuntu" || "$ID_LIKE" == *"ubuntu"* || "$ID" == "pop" || "$ID" == "linuxmint" || "$ID" == "elementary" || "$ID" == "zorin" ]]; then
|
||
DISTRO_FAMILY="ubuntu"
|
||
elif [[ "$ID" == "debian" || "$ID_LIKE" == *"debian"* ]]; then
|
||
DISTRO_FAMILY="debian"
|
||
elif [[ "$ID" == "fedora" || "$ID_LIKE" == *"fedora"* || "$ID" == "rhel" || "$ID" == "centos" || "$ID" == "rocky" || "$ID" == "almalinux" ]]; then
|
||
DISTRO_FAMILY="fedora"
|
||
elif [[ "$ID" == "arch" || "$ID_LIKE" == *"arch"* || "$ID" == "manjaro" || "$ID" == "endeavouros" ]]; then
|
||
DISTRO_FAMILY="arch"
|
||
elif [[ "$ID" == "opensuse" || "$ID_LIKE" == *"suse"* ]]; then
|
||
DISTRO_FAMILY="suse"
|
||
elif [[ "$ID" == "gentoo" ]]; then
|
||
DISTRO_FAMILY="gentoo"
|
||
elif [[ "$ID" == "alpine" ]]; then
|
||
DISTRO_FAMILY="alpine"
|
||
elif [[ "$ID" == "void" ]]; then
|
||
DISTRO_FAMILY="void"
|
||
elif [[ "$ID" == "solus" ]]; then
|
||
DISTRO_FAMILY="solus"
|
||
elif [[ "$ID" == "mageia" ]]; then
|
||
DISTRO_FAMILY="mageia"
|
||
fi
|
||
|
||
print_info "Detected: $DISTRO_NAME $DISTRO_VERSION ($DISTRO_FAMILY family)"
|
||
return 0
|
||
else
|
||
print_error "Could not detect Linux distribution (missing /etc/os-release)"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Check minimum required version for Ubuntu-based systems
|
||
check_ubuntu_version() {
|
||
local MIN_VERSION="18.04"
|
||
if [[ "$DISTRO_FAMILY" == "ubuntu" ]]; then
|
||
if [[ $(echo -e "$DISTRO_VERSION\n$MIN_VERSION" | sort -V | head -n1) == "$MIN_VERSION" || "$DISTRO_VERSION" == "$MIN_VERSION" ]]; then
|
||
return 0
|
||
else
|
||
print_error "This application requires Ubuntu $MIN_VERSION or newer. Detected: $DISTRO_VERSION"
|
||
return 1
|
||
fi
|
||
fi
|
||
return 0
|
||
}
|
||
|
||
# Detect NVIDIA GPU presence
|
||
detect_nvidia_gpu() {
|
||
# Check if nvidia-smi command exists and can successfully query GPU
|
||
if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi >/dev/null 2>&1; then
|
||
# Extract GPU information for user feedback
|
||
GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -n1)
|
||
GPU_MEMORY=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader 2>/dev/null | head -n1)
|
||
HAS_NVIDIA_GPU="yes"
|
||
return 0
|
||
else
|
||
HAS_NVIDIA_GPU="no"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Detect Vulkan support for whisper.cpp
|
||
detect_vulkan() {
|
||
# Check for vulkaninfo command
|
||
if command -v vulkaninfo >/dev/null 2>&1; then
|
||
local vulkan_output=$(vulkaninfo --summary 2>/dev/null | head -20)
|
||
if [ -n "$vulkan_output" ]; then
|
||
HAS_VULKAN="yes"
|
||
# Try to extract GPU name
|
||
VULKAN_DEVICE=$(echo "$vulkan_output" | grep -i "deviceName" | head -1 | cut -d'=' -f2 | xargs)
|
||
if [ -z "$VULKAN_DEVICE" ]; then
|
||
VULKAN_DEVICE="Vulkan-compatible GPU"
|
||
fi
|
||
return 0
|
||
fi
|
||
fi
|
||
HAS_VULKAN="no"
|
||
return 1
|
||
}
|
||
|
||
# Check for incompatible Intel GPUs that don't support VK_KHR_16bit_storage
|
||
# These GPUs will fail with "device does not support 16-bit storage" error
|
||
# Affected: Intel Gen7 and older (Ivy Bridge, Haswell, Sandy Bridge)
|
||
# See: https://github.com/jatinkrmalik/vocalinux/issues/238
|
||
#
|
||
# IMPORTANT: This check filters out software renderers (llvmpipe, etc.) and only
|
||
# evaluates real hardware GPUs. Modern AMD, Intel (Gen8+), and NVIDIA GPUs all
|
||
# support VK_KHR_16bit_storage, so this mainly catches very old Intel Gen7 GPUs.
|
||
check_vulkan_gpu_compatibility() {
|
||
# List of known incompatible GPU patterns (old Intel Gen7 and older)
|
||
local INCOMPATIBLE_PATTERNS=(
|
||
"Ivy Bridge"
|
||
"Haswell"
|
||
"Sandy Bridge"
|
||
"HD Graphics 2500"
|
||
"HD Graphics 4000"
|
||
"HD Graphics 4400"
|
||
"HD Graphics 4600"
|
||
"HD Graphics P4600"
|
||
"HD Graphics P4700"
|
||
"IVB"
|
||
"HSW"
|
||
"SNB"
|
||
)
|
||
|
||
# Software renderers and virtual devices to skip (not real hardware GPUs)
|
||
# These are CPU-based implementations that shouldn't affect compatibility detection
|
||
local SOFTWARE_RENDERER_PATTERNS=(
|
||
"llvmpipe"
|
||
"swiftshader"
|
||
"lavapipe"
|
||
"zink"
|
||
"virtio"
|
||
"venus"
|
||
)
|
||
|
||
# Check if vulkaninfo is available
|
||
if ! command -v vulkaninfo >/dev/null 2>&1; then
|
||
echo "unknown:vulkaninfo not available"
|
||
return 1
|
||
fi
|
||
|
||
# Get all device names from vulkaninfo
|
||
local DEVICE_NAMES_RAW
|
||
DEVICE_NAMES_RAW=$(vulkaninfo --summary 2>/dev/null | awk -F'=' '/deviceName/ {gsub(/^[ \t]+|[ \t]+$/, "", $2); if ($2 != "") print $2}')
|
||
|
||
# Separate hardware GPUs from software renderers
|
||
local HARDWARE_GPUS=""
|
||
local HARDWARE_GPU_COUNT=0
|
||
while IFS= read -r device_name; do
|
||
[ -z "$device_name" ] && continue
|
||
|
||
local is_software=false
|
||
for pattern in "${SOFTWARE_RENDERER_PATTERNS[@]}"; do
|
||
if echo "$device_name" | grep -iq "$pattern"; then
|
||
is_software=true
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ "$is_software" = false ]; then
|
||
if [ -n "$HARDWARE_GPUS" ]; then
|
||
HARDWARE_GPUS="${HARDWARE_GPUS}, ${device_name}"
|
||
else
|
||
HARDWARE_GPUS="$device_name"
|
||
fi
|
||
((HARDWARE_GPU_COUNT++))
|
||
fi
|
||
done <<< "$DEVICE_NAMES_RAW"
|
||
|
||
# If no hardware GPUs found, we can't determine compatibility
|
||
if [ -z "$HARDWARE_GPUS" ]; then
|
||
echo "unknown:No hardware GPU found (only software renderers)"
|
||
return 1
|
||
fi
|
||
|
||
# Get Vulkan features and check for VK_KHR_16bit_storage
|
||
# Modern GPUs (AMD, Intel Gen8+, NVIDIA) all support this extension
|
||
local FEATURES_OUTPUT
|
||
FEATURES_OUTPUT=$(vulkaninfo --features 2>/dev/null)
|
||
|
||
if [ -n "$FEATURES_OUTPUT" ]; then
|
||
# Check for VK_KHR_16bit_storage extension or equivalent features
|
||
if echo "$FEATURES_OUTPUT" | grep -q "VK_KHR_16bit_storage"; then
|
||
echo "compatible:${HARDWARE_GPUS}"
|
||
return 0
|
||
fi
|
||
|
||
# Alternative: check for 16-bit storage features directly
|
||
if echo "$FEATURES_OUTPUT" | grep -Eq "storageBuffer16BitAccess[[:space:]]*=[[:space:]]*true|uniformAndStorageBuffer16BitAccess[[:space:]]*=[[:space:]]*true"; then
|
||
echo "compatible:${HARDWARE_GPUS}"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# If Vulkan features check didn't confirm support, check against known incompatible patterns
|
||
# This handles systems where vulkaninfo --features doesn't show the extension
|
||
local INCOMPATIBLE_GPUS=""
|
||
local HAS_COMPATIBLE_GPU=false
|
||
|
||
while IFS= read -r device_name; do
|
||
[ -z "$device_name" ] && continue
|
||
|
||
# Skip software renderers
|
||
local is_software=false
|
||
for pattern in "${SOFTWARE_RENDERER_PATTERNS[@]}"; do
|
||
if echo "$device_name" | grep -iq "$pattern"; then
|
||
is_software=true
|
||
break
|
||
fi
|
||
done
|
||
[ "$is_software" = true ] && continue
|
||
|
||
# Check against known incompatible patterns
|
||
local is_incompatible=false
|
||
for pattern in "${INCOMPATIBLE_PATTERNS[@]}"; do
|
||
if echo "$device_name" | grep -iq "$pattern"; then
|
||
is_incompatible=true
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [ "$is_incompatible" = true ]; then
|
||
if [ -n "$INCOMPATIBLE_GPUS" ]; then
|
||
INCOMPATIBLE_GPUS="${INCOMPATIBLE_GPUS}, ${device_name}"
|
||
else
|
||
INCOMPATIBLE_GPUS="$device_name"
|
||
fi
|
||
else
|
||
# GPU doesn't match known incompatible patterns - assume compatible
|
||
HAS_COMPATIBLE_GPU=true
|
||
fi
|
||
done <<< "$DEVICE_NAMES_RAW"
|
||
|
||
if [ "$HAS_COMPATIBLE_GPU" = true ]; then
|
||
echo "compatible:${HARDWARE_GPUS}"
|
||
return 0
|
||
fi
|
||
|
||
if [ -n "$INCOMPATIBLE_GPUS" ]; then
|
||
echo "incompatible:${INCOMPATIBLE_GPUS}"
|
||
return 1
|
||
fi
|
||
|
||
echo "unknown:Could not classify Vulkan GPU compatibility"
|
||
return 1
|
||
}
|
||
|
||
# Detect available GPU backends for whisper.cpp and recommend the best option
|
||
detect_whispercpp_backends() {
|
||
detect_nvidia_gpu || true
|
||
detect_vulkan || true
|
||
|
||
# Check for Vulkan dev libraries
|
||
local HAS_VULKAN_DEV=false
|
||
if pkg-config --exists vulkan 2>/dev/null || [ -f /usr/include/vulkan/vulkan.h ]; then
|
||
HAS_VULKAN_DEV=true
|
||
fi
|
||
|
||
# Check for CUDA
|
||
local HAS_CUDA_DEV=false
|
||
if command -v nvcc >/dev/null 2>&1; then
|
||
HAS_CUDA_DEV=true
|
||
fi
|
||
|
||
# Check Vulkan GPU compatibility (Gen7 and older Intel GPUs lack 16-bit storage support)
|
||
# Skip this check for NVIDIA GPUs since they use CUDA, not Vulkan
|
||
local VULKAN_COMPATIBLE="unknown"
|
||
local VULKAN_COMPAT_REASON=""
|
||
if [[ "$HAS_VULKAN" == "yes" && "$HAS_NVIDIA_GPU" != "yes" ]]; then
|
||
local COMPAT_RESULT
|
||
COMPAT_RESULT=$(check_vulkan_gpu_compatibility)
|
||
VULKAN_COMPATIBLE=$(echo "$COMPAT_RESULT" | cut -d':' -f1)
|
||
VULKAN_COMPAT_REASON=$(echo "$COMPAT_RESULT" | cut -d':' -f2-)
|
||
elif [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then
|
||
# NVIDIA GPUs use CUDA, so Vulkan compatibility is irrelevant
|
||
VULKAN_COMPATIBLE="not_applicable"
|
||
VULKAN_COMPAT_REASON="NVIDIA GPU uses CUDA"
|
||
fi
|
||
|
||
# Determine recommendation (Priority: CUDA > Vulkan > CPU)
|
||
# IMPORTANT: The installer WILL install dev libraries (libvulkan-dev, glslc, CUDA) later,
|
||
# so we recommend GPU if there's a compatible GPU regardless of current library status.
|
||
local RECOMMENDED_BACKEND="cpu"
|
||
local RECOMMENDED_REASON=""
|
||
local CAN_BUILD_GPU=false
|
||
|
||
# NVIDIA GPU - best option, uses CUDA
|
||
if [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then
|
||
RECOMMENDED_BACKEND="cuda"
|
||
if [[ "$HAS_CUDA_DEV" == "true" ]]; then
|
||
RECOMMENDED_REASON="NVIDIA GPU with CUDA toolkit installed"
|
||
else
|
||
RECOMMENDED_REASON="NVIDIA GPU detected (CUDA toolkit will be installed)"
|
||
fi
|
||
CAN_BUILD_GPU=true
|
||
# Vulkan-compatible GPU (AMD, Intel Gen8+) - second choice
|
||
elif [[ "$HAS_VULKAN" == "yes" && "$VULKAN_COMPATIBLE" == "compatible" ]]; then
|
||
RECOMMENDED_BACKEND="vulkan"
|
||
if [[ "$HAS_VULKAN_DEV" == "true" ]]; then
|
||
RECOMMENDED_REASON="Vulkan GPU detected with dev libraries"
|
||
else
|
||
RECOMMENDED_REASON="Vulkan GPU detected (dev libraries will be installed)"
|
||
fi
|
||
CAN_BUILD_GPU=true
|
||
# Vulkan GPU but compatibility unknown - allow GPU build as fallback
|
||
elif [[ "$HAS_VULKAN" == "yes" && "$VULKAN_COMPATIBLE" == "unknown" ]]; then
|
||
RECOMMENDED_BACKEND="vulkan"
|
||
RECOMMENDED_REASON="Possible Vulkan GPU (will verify during build)"
|
||
CAN_BUILD_GPU=true
|
||
# Incompatible Vulkan GPU (old Intel Gen7) - CPU only
|
||
elif [[ "$VULKAN_COMPATIBLE" == "incompatible" ]]; then
|
||
RECOMMENDED_BACKEND="cpu"
|
||
RECOMMENDED_REASON="Incompatible GPU ($VULKAN_COMPAT_REASON) - CPU mode recommended"
|
||
CAN_BUILD_GPU=false
|
||
else
|
||
RECOMMENDED_BACKEND="cpu"
|
||
RECOMMENDED_REASON="No compatible GPU detected"
|
||
CAN_BUILD_GPU=false
|
||
fi
|
||
|
||
echo "${RECOMMENDED_BACKEND}:${RECOMMENDED_REASON}:${CAN_BUILD_GPU}:${HAS_VULKAN}:${HAS_NVIDIA_GPU}:${HAS_VULKAN_DEV}:${HAS_CUDA_DEV}:${VULKAN_COMPATIBLE}:${VULKAN_COMPAT_REASON}"
|
||
}
|
||
|
||
# Detect hardware and recommend best engine
|
||
get_engine_recommendation() {
|
||
detect_nvidia_gpu || true
|
||
detect_vulkan || true
|
||
|
||
# Get RAM info
|
||
local TOTAL_RAM_GB=$(free -g 2>/dev/null | awk '/^Mem:/{print $2}' || echo "0")
|
||
|
||
if [[ "$HAS_NVIDIA_GPU" == "yes" ]]; then
|
||
# NVIDIA GPU detected - whisper.cpp can use CUDA
|
||
echo "whisper_cpp:✓:NVIDIA GPU detected ($GPU_NAME) - Best performance with whisper.cpp"
|
||
elif [[ "$HAS_VULKAN" == "yes" ]]; then
|
||
# Non-NVIDIA GPU with Vulkan support
|
||
echo "whisper_cpp:✓:$VULKAN_DEVICE detected - Great performance with whisper.cpp Vulkan"
|
||
elif [ "$TOTAL_RAM_GB" -ge 8 ]; then
|
||
# No GPU but decent RAM
|
||
echo "whisper_cpp:✓:No GPU detected, but ${TOTAL_RAM_GB}GB RAM - whisper.cpp CPU mode"
|
||
else
|
||
# Low RAM, no GPU
|
||
echo "vosk:⚠:Low RAM (${TOTAL_RAM_GB}GB) and no GPU - VOSK recommended for best performance"
|
||
fi
|
||
}
|
||
|
||
# Detect GI_TYPELIB_PATH for cross-distro compatibility
|
||
detect_typelib_path() {
|
||
# Try pkg-config first (most reliable)
|
||
if command -v pkg-config >/dev/null 2>&1; then
|
||
local path=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null)
|
||
if [ -n "$path" ] && [ -d "$path" ]; then
|
||
echo "$path"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
# Fallback to common distribution-specific paths
|
||
# Order matters: more specific paths first
|
||
for path in \
|
||
/usr/lib/x86_64-linux-gnu/girepository-1.0 \
|
||
/usr/lib/aarch64-linux-gnu/girepository-1.0 \
|
||
/usr/lib/arm-linux-gnueabihf/girepository-1.0 \
|
||
/usr/lib/riscv64-linux-gnu/girepository-1.0 \
|
||
/usr/lib/powerpc64le-linux-gnu/girepository-1.0 \
|
||
/usr/lib/s390x-linux-gnu/girepository-1.0 \
|
||
/usr/lib64/girepository-1.0 \
|
||
/usr/lib/girepository-1.0 \
|
||
/usr/local/lib/girepository-1.0 \
|
||
/usr/local/lib64/girepository-1.0; do
|
||
if [ -d "$path" ]; then
|
||
echo "$path"
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
# Ultimate fallback - will cause issues if wrong, but at least we try
|
||
echo "/usr/lib/girepository-1.0"
|
||
return 1
|
||
}
|
||
|
||
# Print section header for interactive mode
|
||
clear_screen() {
|
||
if [ -t 1 ] && command -v clear >/dev/null 2>&1 && [ -n "${TERM:-}" ]; then
|
||
clear >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
|
||
print_header() {
|
||
local title="$1"
|
||
echo ""
|
||
echo "============================================================"
|
||
echo " $title"
|
||
echo "============================================================"
|
||
}
|
||
|
||
# Function to run interactive guided installation
|
||
run_interactive_install() {
|
||
clear_screen
|
||
cat << "EOF"
|
||
|
||
Interactive Installation Guide
|
||
===============================
|
||
|
||
EOF
|
||
|
||
echo "Welcome! This guided installation will help you set up Vocalinux"
|
||
echo "with the best options for your system."
|
||
echo ""
|
||
echo "All speech engines are 100% offline, local, and private."
|
||
echo "Your voice data never leaves your computer."
|
||
echo ""
|
||
|
||
# Step 1: Detect and display system info
|
||
print_header "Step 1: Your System"
|
||
echo "Detected: $DISTRO_NAME $DISTRO_VERSION"
|
||
|
||
# Get hardware recommendation
|
||
local RECOMMENDATION=$(get_engine_recommendation)
|
||
local RECOMMENDED_ENGINE=$(echo "$RECOMMENDATION" | cut -d':' -f1)
|
||
local RECOMMENDED_ICON=$(echo "$RECOMMENDATION" | cut -d':' -f2)
|
||
local RECOMMENDED_REASON=$(echo "$RECOMMENDATION" | cut -d':' -f3-)
|
||
|
||
echo "Hardware: $RECOMMENDED_REASON"
|
||
echo ""
|
||
|
||
# Step 2: Choose speech recognition engine
|
||
print_header "Step 2: Choose Speech Recognition Engine"
|
||
echo ""
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 1. WHISPER.CPP ★ RECOMMENDED │"
|
||
echo " │ • Fastest, most accurate, works with any GPU │"
|
||
echo " │ • Supports NVIDIA (CUDA), AMD, Intel (Vulkan) │"
|
||
echo " │ • CPU-only mode available for older systems │"
|
||
echo " │ • Models: tiny (39MB) to large (1.5GB) │"
|
||
echo " │ • 99+ languages with auto-detection │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 2. WHISPER (OpenAI) │"
|
||
echo " │ • PyTorch-based, high accuracy │"
|
||
echo " │ • Only supports NVIDIA GPUs (CUDA) │"
|
||
echo " │ • Larger download (~2GB with CUDA) │"
|
||
echo " │ • Good for development/research │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 3. VOSK │"
|
||
echo " │ • Lightweight and fast │"
|
||
echo " │ • Works on older/low-RAM systems │"
|
||
echo " │ • ~40MB download │"
|
||
echo " │ • Good for basic dictation needs │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 4. REMOTE API (ADVANCED) │"
|
||
echo " │ • Offload processing to a GPU server on your network │"
|
||
echo " │ • Ideal for laptops without GPU │"
|
||
echo " │ • Supports whisper.cpp server & OpenAI-compatible APIs │"
|
||
echo " │ • Minimal local resources needed │"
|
||
echo " │ • Requires a remote server to be running │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
|
||
# Show recommendation
|
||
case "$RECOMMENDED_ENGINE" in
|
||
whisper_cpp)
|
||
echo " → Recommendation: whisper.cpp (best performance for your hardware)"
|
||
DEFAULT_CHOICE="1"
|
||
;;
|
||
vosk)
|
||
echo " → Recommendation: VOSK (lightweight option for your system)"
|
||
DEFAULT_CHOICE="3"
|
||
;;
|
||
*)
|
||
echo " → Recommendation: whisper.cpp (best overall experience)"
|
||
DEFAULT_CHOICE="1"
|
||
;;
|
||
esac
|
||
echo ""
|
||
|
||
read -p "Choose engine [1-4] (default: $DEFAULT_CHOICE): " ENGINE_CHOICE
|
||
ENGINE_CHOICE=${ENGINE_CHOICE:-$DEFAULT_CHOICE}
|
||
|
||
case "$ENGINE_CHOICE" in
|
||
1)
|
||
SELECTED_ENGINE="whisper_cpp"
|
||
ENGINE_DISPLAY="Whisper.cpp (Recommended)"
|
||
;;
|
||
2)
|
||
SELECTED_ENGINE="whisper"
|
||
ENGINE_DISPLAY="Whisper (OpenAI)"
|
||
;;
|
||
3)
|
||
SELECTED_ENGINE="vosk"
|
||
ENGINE_DISPLAY="VOSK (Lightweight)"
|
||
;;
|
||
4)
|
||
SELECTED_ENGINE="remote_api"
|
||
ENGINE_DISPLAY="Remote API"
|
||
;;
|
||
*)
|
||
SELECTED_ENGINE="whisper_cpp"
|
||
ENGINE_DISPLAY="Whisper.cpp (Recommended)"
|
||
;;
|
||
esac
|
||
|
||
# Step 3: Whisper.cpp backend selection (if whisper.cpp chosen)
|
||
if [[ "$SELECTED_ENGINE" == "whisper_cpp" ]]; then
|
||
print_header "Step 3: Choose Whisper.cpp Backend"
|
||
echo ""
|
||
|
||
# Detect available backends
|
||
local BACKEND_INFO=$(detect_whispercpp_backends)
|
||
local RECOMMENDED_BACKEND=$(echo "$BACKEND_INFO" | cut -d':' -f1)
|
||
local RECOMMENDED_REASON=$(echo "$BACKEND_INFO" | cut -d':' -f2)
|
||
local CAN_BUILD_GPU=$(echo "$BACKEND_INFO" | cut -d':' -f3)
|
||
local HAS_VULKAN=$(echo "$BACKEND_INFO" | cut -d':' -f4)
|
||
local HAS_NVIDIA=$(echo "$BACKEND_INFO" | cut -d':' -f5)
|
||
local HAS_VULKAN_DEV=$(echo "$BACKEND_INFO" | cut -d':' -f6)
|
||
local HAS_CUDA_DEV=$(echo "$BACKEND_INFO" | cut -d':' -f7)
|
||
local VULKAN_COMPAT=$(echo "$BACKEND_INFO" | cut -d':' -f8)
|
||
local VULKAN_COMPAT_REASON=$(echo "$BACKEND_INFO" | cut -d':' -f9)
|
||
|
||
# Show warning for incompatible GPUs
|
||
if [[ "$VULKAN_COMPAT" == "incompatible" ]]; then
|
||
echo ""
|
||
print_warning "═══════════════════════════════════════════════════════════════"
|
||
print_warning " ⚠️ INCOMPATIBLE GPU DETECTED"
|
||
print_warning "═══════════════════════════════════════════════════════════════"
|
||
print_warning ""
|
||
print_warning " Your GPU: $VULKAN_COMPAT_REASON"
|
||
print_warning ""
|
||
print_warning " This Intel GPU lacks VK_KHR_16bit_storage support, which is"
|
||
print_warning " required for whisper.cpp Vulkan acceleration."
|
||
print_warning ""
|
||
print_warning " The CPU backend will be used instead, which is still fast!"
|
||
print_warning ""
|
||
print_warning "═══════════════════════════════════════════════════════════════"
|
||
echo ""
|
||
fi
|
||
|
||
echo "Whisper.cpp can use different backends for speech recognition:"
|
||
echo ""
|
||
|
||
if [[ "$CAN_BUILD_GPU" == "true" ]]; then
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 1. GPU (Vulkan/CUDA) ★ RECOMMENDED │"
|
||
echo " │ • Fastest performance with GPU acceleration │"
|
||
echo " │ • $RECOMMENDED_REASON │"
|
||
echo " │ • Requires building from source (takes ~2-5 min) │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 2. CPU (Pre-built) │"
|
||
echo " │ • Works on all systems │"
|
||
echo " │ • Faster installation (no compilation) │"
|
||
echo " │ • Good performance on modern CPUs │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo " → Recommendation: GPU backend for best performance"
|
||
local DEFAULT_BACKEND="1"
|
||
else
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 1. GPU (Vulkan/CUDA) │"
|
||
echo " │ • ⚠️ GPU libraries not detected │"
|
||
echo " │ • Requires: libvulkan-dev, glslc/glslang-tools (Vulkan) │"
|
||
echo " │ or: CUDA toolkit (NVIDIA) │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
echo " ┌─────────────────────────────────────────────────────────────┐"
|
||
echo " │ 2. CPU (Pre-built) ★ RECOMMENDED │"
|
||
echo " │ • Works on all systems │"
|
||
echo " │ • Fast installation (no compilation) │"
|
||
echo " │ • Good performance on modern CPUs │"
|
||
echo " └─────────────────────────────────────────────────────────────┘"
|
||
echo ""
|
||
|
||
if [[ "$HAS_VULKAN" == "yes" && "$HAS_VULKAN_DEV" != "true" ]]; then
|
||
echo " 💡 Tip: Install 'libvulkan-dev' and a shader compiler for GPU support:"
|
||
echo " sudo apt install libvulkan-dev glslc 2>/dev/null || sudo apt install libvulkan-dev glslang-tools"
|
||
echo ""
|
||
elif [[ "$HAS_NVIDIA" == "yes" && "$HAS_CUDA_DEV" != "true" ]]; then
|
||
echo " 💡 Tip: Install CUDA toolkit for NVIDIA GPU support:"
|
||
echo " https://developer.nvidia.com/cuda-downloads"
|
||
echo ""
|
||
fi
|
||
|
||
echo " → Recommendation: CPU backend (GPU libraries not detected)"
|
||
local DEFAULT_BACKEND="2"
|
||
fi
|
||
|
||
read -p "Choose backend [1-2] (default: $DEFAULT_BACKEND): " BACKEND_CHOICE
|
||
BACKEND_CHOICE=${BACKEND_CHOICE:-$DEFAULT_BACKEND}
|
||
|
||
if [[ "$BACKEND_CHOICE" == "1" ]]; then
|
||
WHISPERCPP_BACKEND="gpu"
|
||
BACKEND_DISPLAY="GPU (Vulkan/CUDA)"
|
||
else
|
||
WHISPERCPP_BACKEND="cpu"
|
||
BACKEND_DISPLAY="CPU (Pre-built)"
|
||
fi
|
||
|
||
echo ""
|
||
fi
|
||
|
||
# Step 3 for Remote API: Configure server URL
|
||
if [[ "$SELECTED_ENGINE" == "remote_api" ]]; then
|
||
print_header "Step 3: Configure Remote Server"
|
||
echo ""
|
||
print_info "You need a speech recognition server running on your local network."
|
||
echo ""
|
||
echo " Supported servers:"
|
||
echo " • whisper.cpp server: ./server -m model.bin --host 0.0.0.0 --port 8080"
|
||
echo " • LocalAI: docker run -p 8080:8080 localai/localai"
|
||
echo " • Faster Whisper: faster-whisper-server --host 0.0.0.0 --port 8080"
|
||
echo " • Any OpenAI-compatible speech API"
|
||
echo ""
|
||
read -p "Enter remote server URL (or leave blank to set later): " REMOTE_API_URL_INPUT
|
||
if [ -n "$REMOTE_API_URL_INPUT" ]; then
|
||
REMOTE_API_URL="$REMOTE_API_URL_INPUT"
|
||
REMOTE_DISPLAY="$REMOTE_API_URL"
|
||
else
|
||
REMOTE_API_URL=""
|
||
REMOTE_DISPLAY="(configure later in Settings)"
|
||
fi
|
||
echo ""
|
||
fi
|
||
|
||
# Step 4: Model download preference (skip for remote_api)
|
||
if [[ "$SELECTED_ENGINE" != "remote_api" ]]; then
|
||
print_header "Step 4: Model Download"
|
||
echo ""
|
||
echo "Speech recognition models can be downloaded now or later."
|
||
echo ""
|
||
echo " 1. Download now (recommended)"
|
||
echo " • Faster first run - ready to use immediately"
|
||
echo " • Offline capable right after install"
|
||
echo ""
|
||
echo " 2. Download later"
|
||
echo " • Smaller initial install"
|
||
echo " • Models download automatically on first use"
|
||
echo ""
|
||
|
||
read -p "Download models now? [1-2] (default: 1): " MODELS_CHOICE
|
||
MODELS_CHOICE=${MODELS_CHOICE:-1}
|
||
|
||
if [[ "$MODELS_CHOICE" == "2" ]]; then
|
||
SKIP_MODELS="yes"
|
||
MODELS_DISPLAY="Download on first use"
|
||
else
|
||
MODELS_DISPLAY="Download now (recommended)"
|
||
fi
|
||
else
|
||
# Remote API: No need to download model
|
||
SKIP_MODELS="yes"
|
||
MODELS_DISPLAY="Not needed (remote processing)"
|
||
fi
|
||
|
||
# Summary
|
||
print_header "Installation Summary"
|
||
echo ""
|
||
echo " Speech Engine: $ENGINE_DISPLAY"
|
||
if [[ "$SELECTED_ENGINE" == "whisper_cpp" ]]; then
|
||
echo " Backend: ${BACKEND_DISPLAY:-CPU (Pre-built)}"
|
||
if [[ "${WHISPERCPP_BACKEND}" == "gpu" ]]; then
|
||
echo " Note: GPU build will compile from source (2-5 minutes)"
|
||
fi
|
||
fi
|
||
if [[ "$SELECTED_ENGINE" == "remote_api" ]]; then
|
||
echo " Remote Server: $REMOTE_DISPLAY"
|
||
fi
|
||
echo " Models: $MODELS_DISPLAY"
|
||
echo " Install Location: ${INSTALL_DIR:-\$HOME/.local/share/vocalinux}"
|
||
echo ""
|
||
read -p "Press Enter to continue with installation, or Ctrl+C to cancel..."
|
||
echo ""
|
||
}
|
||
|
||
# Detect distribution
|
||
detect_distro
|
||
|
||
# Check compatibility
|
||
if [[ "$DISTRO_FAMILY" == "debian" ]]; then
|
||
print_info "Detected Debian — fully supported. Continuing with Debian-specific configuration."
|
||
elif [[ "$DISTRO_FAMILY" != "ubuntu" ]]; then
|
||
print_warning "This installer is primarily designed for Ubuntu/Debian-based systems. Your system: $DISTRO_NAME"
|
||
print_warning "The application may still work, but you might need to install dependencies manually."
|
||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||
print_info "Non-interactive mode: continuing anyway..."
|
||
else
|
||
read -p "Do you want to continue anyway? (y/n) " -n 1 -r
|
||
echo
|
||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||
exit 1
|
||
fi
|
||
fi
|
||
else
|
||
# Check version for Ubuntu-based systems
|
||
if ! check_ubuntu_version; then
|
||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||
print_info "Non-interactive mode: continuing anyway..."
|
||
else
|
||
read -p "Do you want to continue anyway? (y/n) " -n 1 -r
|
||
echo
|
||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||
exit 1
|
||
fi
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Handle installation mode selection
|
||
if [[ "$INTERACTIVE_MODE" == "ask" ]]; then
|
||
# Running via curl pipe but we have a terminal - ask user preference
|
||
echo ""
|
||
echo "Installation Mode:"
|
||
echo " 1. Interactive (recommended) - guided setup with recommendations"
|
||
echo " 2. Automatic - quick install with defaults (whisper.cpp)"
|
||
echo ""
|
||
read -p "Choose mode [1-2] (default: 1): " MODE_CHOICE
|
||
MODE_CHOICE=${MODE_CHOICE:-1}
|
||
|
||
if [[ "$MODE_CHOICE" == "2" ]]; then
|
||
AUTO_MODE="yes"
|
||
INTERACTIVE_MODE="no"
|
||
NON_INTERACTIVE="yes"
|
||
else
|
||
INTERACTIVE_MODE="yes"
|
||
NON_INTERACTIVE="no"
|
||
fi
|
||
echo ""
|
||
fi
|
||
|
||
# Run interactive installation if selected
|
||
if [[ "$INTERACTIVE_MODE" == "yes" ]]; then
|
||
# Check if we have a TTY (required for interactive mode)
|
||
if [ ! -t 0 ]; then
|
||
print_error "Interactive mode requires a terminal (TTY)."
|
||
print_error "Download and run the installer directly from a terminal:"
|
||
print_error " curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh -o /tmp/vl.sh && bash /tmp/vl.sh"
|
||
exit 1
|
||
fi
|
||
|
||
# Run interactive installation
|
||
run_interactive_install
|
||
fi
|
||
|
||
# Set default engine for auto/non-interactive mode
|
||
if [[ "$NON_INTERACTIVE" == "yes" ]] && [[ -z "$SELECTED_ENGINE" ]]; then
|
||
# Default to whisper.cpp for best performance
|
||
SELECTED_ENGINE="whisper_cpp"
|
||
print_info "Automatic mode: Installing with whisper.cpp (default engine)"
|
||
print_info "For other engines, use: --engine=whisper or --engine=vosk or --engine=remote_api"
|
||
echo ""
|
||
fi
|
||
|
||
# Function to check if a command exists
|
||
command_exists() {
|
||
command -v "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
# Function to check if a package is installed (for apt-based systems)
|
||
apt_package_installed() {
|
||
dpkg -s "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
# Function to check if a package is installed (for dnf-based systems)
|
||
dnf_package_installed() {
|
||
rpm -q "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
# Function to check if a package is installed (for pacman-based systems)
|
||
pacman_package_installed() {
|
||
pacman -Q "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
suse_python_package_prefix() {
|
||
python3 -c 'import sys; print(f"python{sys.version_info.major}{sys.version_info.minor}")' 2>/dev/null || echo "python3"
|
||
}
|
||
|
||
suse_python_package_candidates() {
|
||
local suffix="$1"
|
||
local PY_PREFIX
|
||
PY_PREFIX=$(suse_python_package_prefix)
|
||
|
||
if [[ "$PY_PREFIX" != "python3" ]]; then
|
||
echo "${PY_PREFIX}-${suffix} python3-${suffix}"
|
||
else
|
||
echo "python3-${suffix}"
|
||
fi
|
||
}
|
||
|
||
suse_package_installed() {
|
||
rpm -q "$1" >/dev/null 2>&1
|
||
}
|
||
|
||
suse_install_first_available() {
|
||
local DESCRIPTION="$1"
|
||
shift
|
||
|
||
local PKG
|
||
for PKG in "$@"; do
|
||
[ -z "$PKG" ] && continue
|
||
|
||
if suse_package_installed "$PKG"; then
|
||
print_info "$DESCRIPTION is already installed ($PKG)."
|
||
return 0
|
||
fi
|
||
|
||
if sudo zypper install -y "$PKG" 2>/dev/null; then
|
||
print_success "Installed $DESCRIPTION ($PKG)."
|
||
return 0
|
||
fi
|
||
|
||
print_info "$DESCRIPTION package '$PKG' not available, trying next option..."
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
suse_appindicator_gi_available() {
|
||
python3 - <<'PY' >/dev/null 2>&1
|
||
import importlib
|
||
import gi
|
||
|
||
for namespace in ("AppIndicator3", "AyatanaAppIndicator3", "AyatanaAppindicator3"):
|
||
try:
|
||
gi.require_version(namespace, "0.1")
|
||
importlib.import_module(f"gi.repository.{namespace}")
|
||
raise SystemExit(0)
|
||
except (ImportError, ValueError):
|
||
pass
|
||
|
||
raise SystemExit(1)
|
||
PY
|
||
}
|
||
|
||
suse_install_appindicator_runtime() {
|
||
local APPINDICATOR_PACKAGES=(
|
||
"typelib-1_0-AyatanaAppIndicator3-0_1"
|
||
"typelib-1_0-AppIndicator3-0_1"
|
||
"typelib-1_0-AyatanaAppIndicator-0_1"
|
||
"libayatana-appindicator3-1"
|
||
"libappindicator3-1"
|
||
"libappindicator-gtk3"
|
||
)
|
||
|
||
if suse_appindicator_gi_available; then
|
||
print_info "AppIndicator/Ayatana GI namespace is already available."
|
||
return 0
|
||
fi
|
||
|
||
local PKG
|
||
for PKG in "${APPINDICATOR_PACKAGES[@]}"; do
|
||
if suse_package_installed "$PKG"; then
|
||
print_info "AppIndicator/Ayatana package is already installed ($PKG); verifying GI namespace..."
|
||
elif sudo zypper install -y "$PKG" 2>/dev/null; then
|
||
print_success "Installed AppIndicator/Ayatana package ($PKG)."
|
||
# Refresh the shared-library cache so the GI typelib is discoverable
|
||
sudo ldconfig 2>/dev/null || true
|
||
else
|
||
print_info "AppIndicator/Ayatana package '$PKG' not available, trying next option..."
|
||
continue
|
||
fi
|
||
|
||
if suse_appindicator_gi_available; then
|
||
print_success "AppIndicator/Ayatana GI namespace is available."
|
||
return 0
|
||
fi
|
||
done
|
||
|
||
return 1
|
||
}
|
||
|
||
suse_shader_compiler_available() {
|
||
command_exists glslc || command_exists glslangValidator
|
||
}
|
||
|
||
# Function to install system dependencies based on the detected distribution
|
||
install_system_dependencies() {
|
||
print_info "Installing system dependencies..."
|
||
|
||
# Determine which Vulkan shader package is available (glslc for Ubuntu 24.04+, glslang-tools for 22.04)
|
||
local VULKAN_SHADER_PKG="glslang-tools" # Default fallback
|
||
if apt-cache show glslc &>/dev/null 2>&1; then
|
||
VULKAN_SHADER_PKG="glslc"
|
||
fi
|
||
|
||
# Define package names for different distributions
|
||
# Determine the correct GObject Introspection dev package for Ubuntu-family distros
|
||
# Ubuntu 24.04+ and Pop!_OS Cosmic+ ship libgirepository-2.0-dev instead of 1.0
|
||
local GI_DEV_PKG="libgirepository1.0-dev"
|
||
if apt-cache show libgirepository-2.0-dev &>/dev/null 2>&1; then
|
||
if ! apt-cache show libgirepository1.0-dev &>/dev/null 2>&1; then
|
||
GI_DEV_PKG="libgirepository-2.0-dev"
|
||
fi
|
||
fi
|
||
|
||
# libssl-dev, autoconf, automake, libtool, patchelf are required for pywhispercpp source
|
||
# builds on Debian. On Ubuntu these are typically pulled in transitively, but on a clean
|
||
# Debian install they are absent and cause CMake's bootstrap to fail (Hurdle 2 from
|
||
# https://medium.com/@cslev/talking-to-my-linux-box-without-talking-to-the-cloud-vocalinux-on-debian-without-the-tears-10bf053ea21b).
|
||
local PYWHISPERCPP_BUILD_DEPS="libssl-dev autoconf automake libtool patchelf"
|
||
local APT_PACKAGES_UBUNTU="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-appindicator3-0.1 gir1.2-ibus-1.0 $GI_DEV_PKG libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev $VULKAN_SHADER_PKG xclip wl-clipboard $PYWHISPERCPP_BUILD_DEPS"
|
||
local APT_PACKAGES_DEBIAN_BASE="python3-pip python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-ibus-1.0 libcairo2-dev cmake python3-dev build-essential portaudio19-dev python3-venv pkg-config wget curl unzip vulkan-tools libvulkan-dev $VULKAN_SHADER_PKG xclip wl-clipboard $PYWHISPERCPP_BUILD_DEPS"
|
||
local APT_PACKAGES_DEBIAN_11_12="$APT_PACKAGES_DEBIAN_BASE libgirepository1.0-dev gir1.2-ayatanaappindicator3-0.1"
|
||
local APT_PACKAGES_DEBIAN_13_PLUS="$APT_PACKAGES_DEBIAN_BASE libgirepository-2.0-dev gir1.2-ayatanaappindicator3-0.1"
|
||
local DNF_PACKAGES="python3-pip python3-gobject gtk3 libappindicator-gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslang xclip wl-clipboard"
|
||
local PACMAN_PACKAGES="python-pip python-gobject gtk3 libappindicator-gtk3 ibus gobject-introspection python-cairo portaudio python-virtualenv pkg-config cmake wget curl unzip base-devel vulkan-tools vulkan-headers glslang xclip wl-clipboard"
|
||
local ZYPPER_PACKAGES="gtk3 ibus-devel gobject-introspection-devel portaudio-devel pkg-config cmake wget curl unzip xclip wl-clipboard typelib-1_0-Notify-0_7 libnotify4"
|
||
# Gentoo uses Portage and different package naming convention
|
||
local EMERGE_PACKAGES="dev-python/pygobject:3 x11-libs/gtk+:3 dev-libs/libayatana-appindicator media-libs/portaudio dev-lang/python:3.9 pkgconf cmake dev-util/glslang x11-misc/xclip gui-apps/wl-clipboard"
|
||
# Alpine Linux uses apk and has musl libc
|
||
local APK_PACKAGES="py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev py3-virtualenv pkgconf cmake wget curl unzip glslang vulkan-tools xclip wl-clipboard"
|
||
# Void Linux uses xbps
|
||
local XBPS_PACKAGES="python3-pip python3-gobject gtk+3 libappindicator-gtk3 gobject-introspection portaudio-devel python3-devel pkg-config cmake wget curl unzip glslang Vulkan-Tools xclip wl-clipboard"
|
||
# Solus uses eopkg
|
||
local EOPKG_PACKAGES="python3-pip python3-gobject gtk3 libappindicator gobject-introspection-devel portaudio-devel python3-virtualenv pkg-config cmake wget curl unzip glslang vulkan-tools xclip wl-clipboard"
|
||
|
||
local MISSING_PACKAGES=""
|
||
local INSTALL_CMD=""
|
||
local UPDATE_CMD=""
|
||
|
||
case "$DISTRO_FAMILY" in
|
||
ubuntu|debian)
|
||
local APT_PACKAGES="$APT_PACKAGES_UBUNTU"
|
||
if [[ "$DISTRO_FAMILY" == "debian" ]]; then
|
||
local DEBIAN_MAJOR="${DISTRO_VERSION%%.*}"
|
||
if [[ "$DEBIAN_MAJOR" =~ ^[0-9]+$ ]] && [ "$DEBIAN_MAJOR" -ge 13 ]; then
|
||
APT_PACKAGES="$APT_PACKAGES_DEBIAN_13_PLUS"
|
||
else
|
||
APT_PACKAGES="$APT_PACKAGES_DEBIAN_11_12"
|
||
fi
|
||
fi
|
||
|
||
# Check for missing packages
|
||
for pkg in $APT_PACKAGES; do
|
||
if ! apt_package_installed "$pkg"; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing missing packages:$MISSING_PACKAGES"
|
||
sudo apt update || { print_error "Failed to update package lists"; exit 1; }
|
||
|
||
# Handle appindicator package for Ubuntu (old package deprecated in newer releases)
|
||
if echo "$MISSING_PACKAGES" | grep -q "gir1.2-appindicator3-0.1"; then
|
||
FILTERED_PACKAGES=$(echo "$MISSING_PACKAGES" | sed 's/gir1.2-appindicator3-0.1//' | xargs)
|
||
|
||
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y gir1.2-appindicator3-0.1 2>/dev/null; then
|
||
print_info "gir1.2-appindicator3-0.1 not available, trying gir1.2-ayatanaappindicator3-0.1..."
|
||
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y gir1.2-ayatanaappindicator3-0.1; then
|
||
print_error "Failed to install appindicator package (tried both gir1.2-appindicator3-0.1 and gir1.2-ayatanaappindicator3-0.1)"
|
||
exit 1
|
||
fi
|
||
print_info "Successfully installed gir1.2-ayatanaappindicator3-0.1 (modern replacement)"
|
||
fi
|
||
|
||
if [ -n "$FILTERED_PACKAGES" ]; then
|
||
DEBIAN_FRONTEND=noninteractive sudo apt install -y $FILTERED_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
fi
|
||
else
|
||
DEBIAN_FRONTEND=noninteractive sudo apt install -y $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
fi
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
fedora)
|
||
# For Fedora/RHEL-based systems
|
||
if command_exists dnf; then
|
||
INSTALL_CMD="sudo dnf install -y"
|
||
UPDATE_CMD="sudo dnf check-update"
|
||
elif command_exists yum; then
|
||
INSTALL_CMD="sudo yum install -y"
|
||
UPDATE_CMD="sudo yum check-update"
|
||
else
|
||
print_error "No supported package manager found (dnf/yum)"
|
||
exit 1
|
||
fi
|
||
|
||
# Check for missing packages
|
||
for pkg in $DNF_PACKAGES; do
|
||
if ! dnf_package_installed "$pkg"; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing missing packages:$MISSING_PACKAGES"
|
||
$UPDATE_CMD || true # dnf check-update returns 100 if updates available
|
||
$INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
arch)
|
||
# For Arch-based systems
|
||
if ! command_exists pacman; then
|
||
print_error "Pacman package manager not found"
|
||
exit 1
|
||
fi
|
||
|
||
# Check for missing packages
|
||
for pkg in $PACMAN_PACKAGES; do
|
||
if ! pacman_package_installed "$pkg"; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing missing packages:$MISSING_PACKAGES"
|
||
sudo pacman -Sy
|
||
sudo pacman -S --noconfirm $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
suse)
|
||
# For openSUSE
|
||
if ! command_exists zypper; then
|
||
print_error "Zypper package manager not found"
|
||
exit 1
|
||
fi
|
||
|
||
sudo zypper refresh || true
|
||
|
||
if [[ "${SELECTED_ENGINE:-whisper_cpp}" == "whisper_cpp" && "${WHISPERCPP_BACKEND:-}" != "cpu" ]]; then
|
||
ZYPPER_PACKAGES="$ZYPPER_PACKAGES vulkan-tools vulkan-devel"
|
||
fi
|
||
|
||
local MISSING_ZYPPER_PACKAGES=()
|
||
for pkg in $ZYPPER_PACKAGES; do
|
||
if ! suse_package_installed "$pkg"; then
|
||
MISSING_ZYPPER_PACKAGES+=("$pkg")
|
||
fi
|
||
done
|
||
|
||
if [ "${#MISSING_ZYPPER_PACKAGES[@]}" -gt 0 ]; then
|
||
print_info "Installing missing packages: ${MISSING_ZYPPER_PACKAGES[*]}"
|
||
sudo zypper install -y "${MISSING_ZYPPER_PACKAGES[@]}" || {
|
||
print_error "Failed to install openSUSE base dependencies"
|
||
exit 1
|
||
}
|
||
else
|
||
print_info "All base openSUSE packages are already installed."
|
||
fi
|
||
|
||
local PY_PIP_CANDIDATES=()
|
||
local PY_GOBJECT_CANDIDATES=()
|
||
local PY_GOBJECT_CAIRO_CANDIDATES=()
|
||
local PY_DEVEL_CANDIDATES=()
|
||
local PY_VIRTUALENV_CANDIDATES=()
|
||
local PY_VENV_CANDIDATES=()
|
||
|
||
read -r -a PY_PIP_CANDIDATES <<< "$(suse_python_package_candidates "pip")"
|
||
read -r -a PY_GOBJECT_CANDIDATES <<< "$(suse_python_package_candidates "gobject")"
|
||
read -r -a PY_GOBJECT_CAIRO_CANDIDATES <<< "$(suse_python_package_candidates "gobject-cairo")"
|
||
read -r -a PY_DEVEL_CANDIDATES <<< "$(suse_python_package_candidates "devel")"
|
||
read -r -a PY_VIRTUALENV_CANDIDATES <<< "$(suse_python_package_candidates "virtualenv")"
|
||
read -r -a PY_VENV_CANDIDATES <<< "$(suse_python_package_candidates "venv")"
|
||
|
||
print_info "Resolving openSUSE Python packages for $(suse_python_package_prefix)..."
|
||
|
||
if ! suse_install_first_available "Python pip" "${PY_PIP_CANDIDATES[@]}"; then
|
||
print_error "Failed to install Python pip package (tried: ${PY_PIP_CANDIDATES[*]})"
|
||
exit 1
|
||
fi
|
||
|
||
if ! suse_install_first_available "PyGObject bindings" "${PY_GOBJECT_CANDIDATES[@]}"; then
|
||
print_error "Failed to install PyGObject package (tried: ${PY_GOBJECT_CANDIDATES[*]})"
|
||
exit 1
|
||
fi
|
||
|
||
if ! suse_install_first_available "PyGObject Cairo bindings" "${PY_GOBJECT_CAIRO_CANDIDATES[@]}"; then
|
||
print_error "Failed to install PyGObject Cairo package (tried: ${PY_GOBJECT_CAIRO_CANDIDATES[*]})"
|
||
exit 1
|
||
fi
|
||
|
||
if ! suse_install_first_available "Python development headers" "${PY_DEVEL_CANDIDATES[@]}"; then
|
||
print_error "Failed to install Python development headers (tried: ${PY_DEVEL_CANDIDATES[*]})"
|
||
exit 1
|
||
fi
|
||
|
||
if ! suse_install_first_available "Python virtualenv/venv" "${PY_VIRTUALENV_CANDIDATES[@]}" "${PY_VENV_CANDIDATES[@]}"; then
|
||
print_warning "Python virtualenv/venv package was not found (tried: ${PY_VIRTUALENV_CANDIDATES[*]} ${PY_VENV_CANDIDATES[*]})"
|
||
print_warning "Continuing because python3 -m venv may still be available."
|
||
fi
|
||
|
||
if ! suse_install_appindicator_runtime; then
|
||
print_error "Failed to install a working AppIndicator/Ayatana GI runtime on openSUSE."
|
||
print_error "Try manually: sudo zypper install typelib-1_0-AyatanaAppIndicator3-0_1 libayatana-appindicator3-1"
|
||
exit 1
|
||
fi
|
||
|
||
if [[ "${SELECTED_ENGINE:-whisper_cpp}" == "whisper_cpp" && "${WHISPERCPP_BACKEND:-}" != "cpu" ]]; then
|
||
if ! suse_shader_compiler_available; then
|
||
if ! suse_install_first_available "Vulkan shader compiler" shaderc glslang-devel glslang; then
|
||
print_warning "No Vulkan shader compiler found - whisper.cpp Vulkan build may fail"
|
||
print_warning "Install shaderc manually for glslc support if you want GPU acceleration."
|
||
fi
|
||
fi
|
||
|
||
if ! suse_shader_compiler_available; then
|
||
print_warning "glslc/glslangValidator is still unavailable; CPU fallback will be used if Vulkan build fails."
|
||
fi
|
||
fi
|
||
;;
|
||
|
||
gentoo)
|
||
# For Gentoo Linux
|
||
if ! command_exists emerge; then
|
||
print_error "Emerge package manager not found"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Gentoo detected. Installing dependencies..."
|
||
print_warning "Gentoo uses emerge. This may take longer as packages are compiled from source."
|
||
|
||
# Check for missing packages
|
||
MISSING_PACKAGES=""
|
||
for pkg in $EMERGE_PACKAGES; do
|
||
# Gentoo uses qlist to check if packages are installed
|
||
if ! qlist -I "$pkg" >/dev/null 2>&1; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing packages:$MISSING_PACKAGES"
|
||
# Update Portage tree first
|
||
sudo emerge --sync || { print_error "Failed to sync Portage tree"; exit 1; }
|
||
# Install missing packages
|
||
sudo emerge $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
alpine)
|
||
# For Alpine Linux
|
||
if ! command_exists apk; then
|
||
print_error "Apk package manager not found"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Alpine Linux detected."
|
||
print_warning "Alpine uses musl libc. Some Python packages may not have pre-built wheels."
|
||
|
||
# Check for missing packages
|
||
MISSING_PACKAGES=""
|
||
for pkg in $APK_PACKAGES; do
|
||
if ! apk info -e "$pkg" >/dev/null 2>&1; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing packages:$MISSING_PACKAGES"
|
||
sudo apk update || { print_error "Failed to update package indexes"; exit 1; }
|
||
sudo apk add $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
void)
|
||
# For Void Linux
|
||
if ! command_exists xbps; then
|
||
print_error "Xbps package manager not found"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Void Linux detected."
|
||
|
||
# Check for missing packages
|
||
MISSING_PACKAGES=""
|
||
for pkg in $XBPS_PACKAGES; do
|
||
if ! xbps-query "$pkg" >/dev/null 2>&1; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing packages:$MISSING_PACKAGES"
|
||
sudo xbps-install -Sy $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
solus)
|
||
# For Solus
|
||
if ! command_exists eopkg; then
|
||
print_error "Eopkg package manager not found"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "Solus detected."
|
||
|
||
# Check for missing packages
|
||
MISSING_PACKAGES=""
|
||
for pkg in $EOPKG_PACKAGES; do
|
||
if ! eopkg list-installed | grep -qw "$pkg"; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing packages:$MISSING_PACKAGES"
|
||
sudo eopkg install $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
mageia)
|
||
# For Mageia
|
||
if command_exists dnf; then
|
||
INSTALL_CMD="sudo dnf install -y"
|
||
UPDATE_CMD="sudo dnf check-update"
|
||
elif command_exists urpmi; then
|
||
INSTALL_CMD="sudo urpmi --force"
|
||
UPDATE_CMD="sudo urpmi.update -a"
|
||
else
|
||
print_error "No supported package manager found (dnf/urpmi)"
|
||
exit 1
|
||
fi
|
||
|
||
# Use similar packages to Fedora/RHEL
|
||
for pkg in $DNF_PACKAGES; do
|
||
# Mageia uses rpm like Fedora
|
||
if ! rpm -q "$pkg" >/dev/null 2>&1; then
|
||
MISSING_PACKAGES="$MISSING_PACKAGES $pkg"
|
||
fi
|
||
done
|
||
|
||
if [ -n "$MISSING_PACKAGES" ]; then
|
||
print_info "Installing missing packages:$MISSING_PACKAGES"
|
||
$UPDATE_CMD 2>/dev/null || true
|
||
$INSTALL_CMD $MISSING_PACKAGES || { print_error "Failed to install dependencies"; exit 1; }
|
||
else
|
||
print_info "All required packages are already installed."
|
||
fi
|
||
;;
|
||
|
||
*)
|
||
print_error "Unsupported distribution family: $DISTRO_FAMILY"
|
||
print_info ""
|
||
print_info "Your distribution ($DISTRO_NAME) is not officially supported."
|
||
print_info "However, you can still install Vocalinux manually:"
|
||
print_info ""
|
||
print_info "1. Run the dependency checker:"
|
||
print_info " bash scripts/check-system-deps.sh"
|
||
print_info ""
|
||
print_info "2. Install missing dependencies using your package manager"
|
||
print_info ""
|
||
print_info "3. Run the installer with --skip-system-deps:"
|
||
print_info " ./install.sh --skip-system-deps"
|
||
print_info ""
|
||
print_info "4. Or install from source in a virtual environment:"
|
||
print_info " python3 -m venv venv"
|
||
print_info " source venv/bin/activate"
|
||
print_info " pip install -e .[whisper,vad]"
|
||
print_info ""
|
||
print_info "For more information, see the project wiki:"
|
||
print_info " https://github.com/jatinkrmalik/vocalinux/wiki"
|
||
print_info ""
|
||
if [[ "$NON_INTERACTIVE" != "yes" ]]; then
|
||
read -p "Continue anyway? (y/n) " -n 1 -r
|
||
echo
|
||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||
exit 1
|
||
fi
|
||
else
|
||
print_info "Non-interactive mode: continuing (dependencies may be missing)..."
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Install system dependencies
|
||
if [[ "$SKIP_SYSTEM_DEPS" == "yes" ]]; then
|
||
print_warning "Skipping system dependency installation (--skip-system-deps specified)."
|
||
print_warning "Make sure GTK, PyGObject, AppIndicator/Ayatana, PortAudio, and text input tools are installed."
|
||
else
|
||
install_system_dependencies
|
||
fi
|
||
|
||
# Define XDG directories
|
||
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/vocalinux"
|
||
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux"
|
||
DESKTOP_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/applications"
|
||
ICON_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor/scalable/apps"
|
||
|
||
# Function to detect and install text input tools
|
||
install_text_input_tools() {
|
||
if [[ "$SKIP_SYSTEM_DEPS" == "yes" ]]; then
|
||
print_warning "Skipping text input tool installation (--skip-system-deps specified)."
|
||
return 0
|
||
fi
|
||
|
||
# Detect session type more robustly
|
||
local SESSION_TYPE="unknown"
|
||
|
||
# Check XDG_SESSION_TYPE first
|
||
if [ -n "$XDG_SESSION_TYPE" ]; then
|
||
SESSION_TYPE="$XDG_SESSION_TYPE"
|
||
# Check for Wayland-specific environment variables
|
||
elif [ -n "$WAYLAND_DISPLAY" ]; then
|
||
SESSION_TYPE="wayland"
|
||
# Check if X server is running
|
||
elif [ -n "$DISPLAY" ] && command_exists xset && xset q &>/dev/null; then
|
||
SESSION_TYPE="x11"
|
||
# Check loginctl if available
|
||
elif command_exists loginctl; then
|
||
SESSION_TYPE=$(loginctl show-session $(loginctl | grep $(whoami) | awk '{print $1}') -p Type | cut -d= -f2)
|
||
fi
|
||
|
||
print_info "Detected session type: $SESSION_TYPE"
|
||
|
||
# Install appropriate tools based on session type and distribution
|
||
case "$SESSION_TYPE" in
|
||
wayland)
|
||
print_info "Installing Wayland text input tools..."
|
||
case "$DISTRO_FAMILY" in
|
||
ubuntu|debian)
|
||
if ! apt_package_installed "wtype"; then
|
||
DEBIAN_FRONTEND=noninteractive sudo apt install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
fedora)
|
||
if command_exists dnf && ! dnf_package_installed "wtype"; then
|
||
sudo dnf install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
elif command_exists yum && ! rpm -q wtype &>/dev/null; then
|
||
sudo yum install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
arch)
|
||
if ! pacman_package_installed "wtype"; then
|
||
sudo pacman -S --noconfirm wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
suse)
|
||
sudo zypper install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
;;
|
||
gentoo)
|
||
if ! qlist -I wtype >/dev/null 2>&1; then
|
||
sudo emerge wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
alpine)
|
||
if ! apk info -e wtype >/dev/null 2>&1; then
|
||
sudo apk add wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
void)
|
||
if ! xbps-query wtype >/dev/null 2>&1; then
|
||
sudo xbps-install -Sy wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
solus)
|
||
if ! eopkg list-installed | grep -qw wtype; then
|
||
sudo eopkg install wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
mageia)
|
||
if command_exists dnf && ! rpm -q wtype >/dev/null 2>&1; then
|
||
sudo dnf install -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
elif command_exists urpmi && ! rpm -q wtype >/dev/null 2>&1; then
|
||
sudo urpmi -y wtype || { print_warning "Failed to install wtype. Text injection may not work properly."; }
|
||
else
|
||
print_info "wtype is already installed."
|
||
fi
|
||
;;
|
||
*)
|
||
print_warning "Unsupported distribution for Wayland text input tools."
|
||
print_warning "Please install 'wtype' manually for Wayland text input support."
|
||
;;
|
||
esac
|
||
|
||
# Try to install ydotool as additional fallback for Wayland
|
||
# ydotool works better with some compositors (like GNOME) where wtype may fail
|
||
print_info "Attempting to install ydotool for better Wayland compatibility..."
|
||
case "$DISTRO_FAMILY" in
|
||
ubuntu|debian)
|
||
if ! apt_package_installed "ydotool"; then
|
||
if ! DEBIAN_FRONTEND=noninteractive sudo apt install -y ydotool 2>/dev/null; then
|
||
if [[ "$DISTRO_FAMILY" == "debian" ]]; then
|
||
print_warning "ydotool is not packaged in Debian's standard repos."
|
||
print_info "For full Wayland input support, you can compile ydotool from source:"
|
||
print_info " sudo apt install -y git cmake libevdev-dev"
|
||
print_info " git clone https://github.com/ReimuNotMoe/ydotool.git /tmp/ydotool"
|
||
print_info " cmake -S /tmp/ydotool -B /tmp/ydotool/build && sudo cmake --build /tmp/ydotool/build --target install"
|
||
print_info " sudo systemctl enable --now ydotoold"
|
||
print_info "Alternatively, wtype (already installed) will handle most Wayland compositors."
|
||
else
|
||
print_info "ydotool not available in repos (optional)"
|
||
fi
|
||
fi
|
||
fi
|
||
;;
|
||
fedora)
|
||
if command_exists dnf; then
|
||
sudo dnf install -y ydotool 2>/dev/null || print_info "ydotool not available in repos (optional)"
|
||
fi
|
||
;;
|
||
arch)
|
||
if ! pacman_package_installed "ydotool"; then
|
||
sudo pacman -S --noconfirm ydotool 2>/dev/null || print_info "ydotool not available in repos (optional)"
|
||
fi
|
||
;;
|
||
esac
|
||
|
||
# Add user to input group for ydotool/dotool support
|
||
if ! groups | grep -q '\binput\b'; then
|
||
print_info "Adding $USER to 'input' group for text injection..."
|
||
sudo usermod -aG input "$USER" || print_warning "Failed to add user to input group"
|
||
print_warning "You will need to LOG OUT and back in for text injection to work with ydotool/dotool"
|
||
fi
|
||
|
||
# Install udev rule for ydotool/dotool
|
||
if [ ! -f /etc/udev/rules.d/80-dotool.rules ]; then
|
||
print_info "Installing udev rule for input device access..."
|
||
echo 'KERNEL=="uinput", GROUP="input", MODE="0620", OPTIONS+="static_node=uinput"' \
|
||
| sudo tee /etc/udev/rules.d/80-dotool.rules >/dev/null 2>&1 || print_warning "Failed to install udev rule"
|
||
sudo udevadm control --reload 2>/dev/null || true
|
||
sudo udevadm trigger 2>/dev/null || true
|
||
fi
|
||
;;
|
||
|
||
x11|"")
|
||
print_info "Installing X11 text input tools..."
|
||
case "$DISTRO_FAMILY" in
|
||
ubuntu|debian)
|
||
if ! apt_package_installed "xdotool"; then
|
||
DEBIAN_FRONTEND=noninteractive sudo apt install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
fedora)
|
||
if command_exists dnf && ! dnf_package_installed "xdotool"; then
|
||
sudo dnf install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
elif command_exists yum && ! rpm -q xdotool &>/dev/null; then
|
||
sudo yum install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
arch)
|
||
if ! pacman_package_installed "xdotool"; then
|
||
sudo pacman -S --noconfirm xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
suse)
|
||
sudo zypper install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
;;
|
||
gentoo)
|
||
if ! qlist -I xdotool >/dev/null 2>&1; then
|
||
sudo emerge xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
alpine)
|
||
if ! apk info -e xdotool >/dev/null 2>&1; then
|
||
sudo apk add xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
void)
|
||
if ! xbps-query xdotool >/dev/null 2>&1; then
|
||
sudo xbps-install -Sy xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
solus)
|
||
if ! eopkg list-installed | grep -qw xdotool; then
|
||
sudo eopkg install xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
mageia)
|
||
if command_exists dnf && ! rpm -q xdotool >/dev/null 2>&1; then
|
||
sudo dnf install -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
elif command_exists urpmi && ! rpm -q xdotool >/dev/null 2>&1; then
|
||
sudo urpmi -y xdotool || { print_warning "Failed to install xdotool. Text injection may not work properly."; }
|
||
else
|
||
print_info "xdotool is already installed."
|
||
fi
|
||
;;
|
||
*)
|
||
print_warning "Unsupported distribution for X11 text input tools."
|
||
print_warning "Please install 'xdotool' manually for X11 text input support."
|
||
;;
|
||
esac
|
||
;;
|
||
|
||
*)
|
||
print_warning "Unknown session type: $SESSION_TYPE"
|
||
print_warning "Installing both Wayland and X11 text input tools for compatibility..."
|
||
|
||
# Install both tools based on distribution
|
||
case "$DISTRO_FAMILY" in
|
||
ubuntu|debian)
|
||
DEBIAN_FRONTEND=noninteractive sudo apt install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
;;
|
||
fedora|mageia)
|
||
if command_exists dnf; then
|
||
sudo dnf install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
elif command_exists yum; then
|
||
sudo yum install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
fi
|
||
# Mageia also supports urpmi
|
||
if [[ "$DISTRO_FAMILY" == "mageia" ]] && command_exists urpmi; then
|
||
sudo urpmi -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
fi
|
||
;;
|
||
arch)
|
||
sudo pacman -S --noconfirm xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
;;
|
||
suse)
|
||
sudo zypper install -y xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
;;
|
||
gentoo)
|
||
sudo emerge xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
;;
|
||
alpine)
|
||
sudo apk add xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
;;
|
||
void)
|
||
sudo xbps-install -Sy xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
;;
|
||
solus)
|
||
sudo eopkg install xdotool wtype || { print_warning "Failed to install text input tools. Text injection may not work properly."; }
|
||
;;
|
||
*)
|
||
print_warning "Unsupported distribution for text input tools."
|
||
print_warning "Please install 'xdotool' and 'wtype' manually for text input support."
|
||
;;
|
||
esac
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# Install text input tools based on session type
|
||
install_text_input_tools
|
||
|
||
# Create necessary directories
|
||
print_info "Creating application directories..."
|
||
mkdir -p "$CONFIG_DIR"
|
||
mkdir -p "$DATA_DIR/models"
|
||
mkdir -p "$DESKTOP_DIR"
|
||
mkdir -p "$ICON_DIR"
|
||
|
||
# Check Python version
|
||
check_python_version() {
|
||
local MIN_VERSION="3.9"
|
||
local PYTHON_CMD="python3"
|
||
|
||
# Check if python3 command exists
|
||
if ! command_exists python3; then
|
||
print_error "Python 3 is not installed or not in PATH"
|
||
return 1
|
||
fi
|
||
|
||
# Get Python version
|
||
local PY_VERSION=$(python3 -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')")
|
||
print_info "Detected Python version: $PY_VERSION"
|
||
|
||
# Compare versions
|
||
if [[ $(echo -e "$PY_VERSION\n$MIN_VERSION" | sort -V | head -n1) == "$MIN_VERSION" || "$PY_VERSION" == "$MIN_VERSION" ]]; then
|
||
return 0
|
||
else
|
||
print_error "This application requires Python $MIN_VERSION or newer. Detected: $PY_VERSION"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Set up virtual environment with error handling
|
||
setup_virtual_environment() {
|
||
print_info "Setting up Python virtual environment in $VENV_DIR..."
|
||
|
||
# Check if virtual environment already exists
|
||
if [ -d "$VENV_DIR" ] && [ -f "$VENV_DIR/bin/activate" ]; then
|
||
print_warning "Virtual environment already exists in $VENV_DIR"
|
||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||
# In non-interactive mode, reuse existing venv
|
||
print_info "Non-interactive mode: using existing virtual environment."
|
||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; }
|
||
return 0
|
||
else
|
||
read -p "Do you want to recreate it? (y/n) " -n 1 -r
|
||
echo
|
||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||
print_info "Removing existing virtual environment..."
|
||
rm -rf "$VENV_DIR"
|
||
else
|
||
print_info "Using existing virtual environment."
|
||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; }
|
||
return 0
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Create virtual environment
|
||
# Use --system-site-packages to access pre-compiled system packages like PyGObject
|
||
# This avoids build failures with Python 3.13+ where PyGObject may not build from source
|
||
python3 -m venv --system-site-packages "$VENV_DIR" || {
|
||
print_warning "python3 -m venv failed, trying python3 -m virtualenv..."
|
||
python3 -m virtualenv --system-site-packages "$VENV_DIR" || {
|
||
print_error "Failed to create virtual environment. Please check your Python installation."
|
||
exit 1
|
||
}
|
||
}
|
||
|
||
# Activate virtual environment
|
||
source "$VENV_DIR/bin/activate" || { print_error "Failed to activate virtual environment"; exit 1; }
|
||
|
||
# Update pip and setuptools
|
||
print_info "Updating pip, setuptools, and wheel..."
|
||
pip install --upgrade pip setuptools wheel || { print_error "Failed to update pip, setuptools, and wheel"; exit 1; }
|
||
|
||
print_info "Virtual environment activated successfully."
|
||
}
|
||
|
||
# Check Python version
|
||
if ! check_python_version; then
|
||
print_warning "Continuing with unsupported Python version. Some features may not work correctly."
|
||
fi
|
||
|
||
# Set up virtual environment
|
||
setup_virtual_environment
|
||
|
||
# Create activation script for users
|
||
# Put it in ~/.local/bin when running remotely, or current dir when running locally
|
||
if [[ "$CLEANUP_ON_EXIT" == "yes" ]]; then
|
||
ACTIVATION_SCRIPT_DIR="$HOME/.local/bin"
|
||
mkdir -p "$ACTIVATION_SCRIPT_DIR"
|
||
else
|
||
ACTIVATION_SCRIPT_DIR="."
|
||
fi
|
||
ACTIVATION_SCRIPT="$ACTIVATION_SCRIPT_DIR/activate-vocalinux.sh"
|
||
|
||
cat > "$ACTIVATION_SCRIPT" << EOF
|
||
#!/bin/bash
|
||
# This script activates the Vocalinux virtual environment
|
||
source "$VENV_DIR/bin/activate"
|
||
echo "Vocalinux virtual environment activated."
|
||
echo "To start the application, run: vocalinux"
|
||
EOF
|
||
chmod +x "$ACTIVATION_SCRIPT"
|
||
print_info "Created activation script: $ACTIVATION_SCRIPT"
|
||
|
||
get_pywhispercpp_library_path() {
|
||
[ -x "$VENV_DIR/bin/python" ] || return 1
|
||
"$VENV_DIR/bin/python" - <<'PY' 2>/dev/null
|
||
from pathlib import Path
|
||
import site
|
||
import sys
|
||
import sysconfig
|
||
|
||
roots = []
|
||
for attr in ("getsitepackages",):
|
||
get_paths = getattr(site, attr, None)
|
||
if get_paths is None:
|
||
continue
|
||
try:
|
||
roots.extend(get_paths())
|
||
except Exception:
|
||
pass
|
||
|
||
user_site = getattr(site, "getusersitepackages", lambda: None)()
|
||
if user_site:
|
||
roots.append(user_site)
|
||
|
||
for key in ("platlib", "purelib"):
|
||
path = sysconfig.get_paths().get(key)
|
||
if path:
|
||
roots.append(path)
|
||
|
||
roots.extend(path for path in sys.path if path)
|
||
|
||
dirs = []
|
||
seen = set()
|
||
for root in roots:
|
||
root_path = Path(root)
|
||
candidates = [
|
||
root_path / "pywhispercpp.libs",
|
||
root_path / "pywhispercpp" / ".libs",
|
||
root_path / "pywhispercpp" / "lib",
|
||
root_path,
|
||
]
|
||
for candidate in candidates:
|
||
try:
|
||
resolved = str(candidate.resolve())
|
||
except OSError:
|
||
continue
|
||
if resolved in seen or not candidate.is_dir():
|
||
continue
|
||
if any(candidate.glob("libwhisper*.so*")) or any(candidate.glob("libggml*.so*")):
|
||
seen.add(resolved)
|
||
dirs.append(resolved)
|
||
|
||
print(":".join(dirs))
|
||
PY
|
||
}
|
||
|
||
is_pywhispercpp_gpu_capable() {
|
||
local LIB_DIRS
|
||
LIB_DIRS=$(get_pywhispercpp_library_path || true)
|
||
[ -n "$LIB_DIRS" ] || return 1
|
||
|
||
local IFS=:
|
||
for dir in $LIB_DIRS; do
|
||
if [ -f "$dir/libggml-vulkan.so" ] || [ -f "$dir/libggml-cuda.so" ]; then
|
||
return 0
|
||
fi
|
||
done
|
||
return 1
|
||
}
|
||
|
||
with_pywhispercpp_library_path() {
|
||
local PYWHISPERCPP_LIBRARY_PATH
|
||
PYWHISPERCPP_LIBRARY_PATH=$(get_pywhispercpp_library_path || true)
|
||
|
||
if [ -n "$PYWHISPERCPP_LIBRARY_PATH" ]; then
|
||
LD_LIBRARY_PATH="$PYWHISPERCPP_LIBRARY_PATH${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" "$@"
|
||
else
|
||
"$@"
|
||
fi
|
||
}
|
||
|
||
get_pywhispercpp_cmake_args() {
|
||
printf '%s\n' '-DCMAKE_INSTALL_RPATH=$ORIGIN -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON'
|
||
}
|
||
|
||
install_cpu_pywhispercpp() {
|
||
local PIP_LOG_FILE="$1"
|
||
local PYWHISPERCPP_CMAKE_ARGS
|
||
PYWHISPERCPP_CMAKE_ARGS=$(get_pywhispercpp_cmake_args)
|
||
|
||
CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" \
|
||
pip install --force-reinstall --no-cache-dir pywhispercpp --log "$PIP_LOG_FILE"
|
||
}
|
||
|
||
is_pywhispercpp_installed() {
|
||
[ -x "$VENV_DIR/bin/python" ] || return 1
|
||
with_pywhispercpp_library_path "$VENV_DIR/bin/python" -c "from pywhispercpp.model import Model" >/dev/null 2>&1
|
||
}
|
||
|
||
get_pywhispercpp_version() {
|
||
[ -x "$VENV_DIR/bin/python" ] || return 1
|
||
"$VENV_DIR/bin/python" - <<'PY' 2>/dev/null
|
||
from importlib import metadata
|
||
|
||
try:
|
||
print(metadata.version("pywhispercpp"))
|
||
except metadata.PackageNotFoundError:
|
||
raise SystemExit(1)
|
||
PY
|
||
}
|
||
|
||
should_rebuild_whispercpp() {
|
||
local INSTALLED_VERSION
|
||
INSTALLED_VERSION=$(get_pywhispercpp_version || true)
|
||
|
||
print_success "Found existing pywhispercpp installation${INSTALLED_VERSION:+ (version $INSTALLED_VERSION)}"
|
||
|
||
case "$REBUILD_WHISPERCPP" in
|
||
yes)
|
||
print_info "Rebuilding pywhispercpp because --rebuild-whispercpp was specified."
|
||
return 0
|
||
;;
|
||
no)
|
||
print_info "Reusing existing pywhispercpp installation."
|
||
return 1
|
||
;;
|
||
esac
|
||
|
||
if [[ "$NON_INTERACTIVE" == "yes" ]]; then
|
||
if [[ "$HAS_NVIDIA_GPU" == "yes" || "$HAS_VULKAN" == "yes" ]] && ! is_pywhispercpp_gpu_capable; then
|
||
print_info "Non-interactive mode: GPU detected but pywhispercpp lacks GPU support. Rebuilding..."
|
||
return 0
|
||
fi
|
||
print_info "Non-interactive mode: reusing existing pywhispercpp installation."
|
||
print_info "Use --rebuild-whispercpp to force a rebuild."
|
||
return 1
|
||
fi
|
||
|
||
if [[ "$HAS_NVIDIA_GPU" == "yes" || "$HAS_VULKAN" == "yes" ]] && ! is_pywhispercpp_gpu_capable; then
|
||
print_info "GPU detected but pywhispercpp lacks GPU support. Rebuilding..."
|
||
return 0
|
||
fi
|
||
|
||
read -p "Rebuild/reinstall pywhispercpp? This can take several minutes. (y/N) " -n 1 -r
|
||
echo
|
||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||
return 0
|
||
fi
|
||
|
||
print_info "Reusing existing pywhispercpp installation."
|
||
return 1
|
||
}
|
||
|
||
install_whispercpp_with_gpu_support() {
|
||
local PIP_LOG_FILE="$1"
|
||
|
||
print_info ""
|
||
print_info "╔════════════════════════════════════════════════════════╗"
|
||
print_info "║ Installing WHISPER.CPP (Recommended) ║"
|
||
print_info "╠════════════════════════════════════════════════════════╣"
|
||
print_info "║ • Fastest speech recognition ║"
|
||
print_info "║ • Works with any GPU: NVIDIA, AMD, Intel ║"
|
||
print_info "║ • Uses Vulkan for GPU acceleration ║"
|
||
print_info "║ • CPU-only mode available ║"
|
||
print_info "╚════════════════════════════════════════════════════════╝"
|
||
print_info ""
|
||
|
||
# Detect GPU and install pywhispercpp with appropriate GPU support
|
||
detect_nvidia_gpu || true
|
||
detect_vulkan || true
|
||
|
||
local GPU_BACKEND="CPU"
|
||
local GPU_INSTALL_SUCCESS=false
|
||
local SKIP_WHISPERCPP_INSTALL=false
|
||
local PYWHISPERCPP_CMAKE_ARGS
|
||
PYWHISPERCPP_CMAKE_ARGS=$(get_pywhispercpp_cmake_args)
|
||
|
||
if [[ "$WHISPERCPP_ALREADY_INSTALLED" == "true" ]]; then
|
||
GPU_BACKEND="existing"
|
||
if should_rebuild_whispercpp; then
|
||
print_info "Existing pywhispercpp will be replaced."
|
||
else
|
||
SKIP_WHISPERCPP_INSTALL=true
|
||
fi
|
||
fi
|
||
|
||
# Check if user explicitly chose CPU backend in interactive mode
|
||
if [[ "$SKIP_WHISPERCPP_INSTALL" == "true" ]]; then
|
||
print_info "Skipping pywhispercpp reinstall; existing compiled bindings remain in place."
|
||
elif [[ "${WHISPERCPP_BACKEND}" == "cpu" ]]; then
|
||
print_info "ℹ Installing CPU-only version (as requested)..."
|
||
GPU_BACKEND="CPU"
|
||
else
|
||
# Try Vulkan first (works with all GPUs: NVIDIA, AMD, Intel)
|
||
if [[ "$HAS_VULKAN" == "yes" ]]; then
|
||
print_info "✓ Vulkan detected: $VULKAN_DEVICE"
|
||
print_info " Installing pywhispercpp with Vulkan support..."
|
||
GPU_BACKEND="Vulkan"
|
||
print_info "Installing pywhispercpp ($GPU_BACKEND backend)..."
|
||
if CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" GGML_VULKAN=1 pip install --force-reinstall --no-cache-dir git+https://github.com/absadiki/pywhispercpp --log "$PIP_LOG_FILE" 2>&1; then
|
||
GPU_INSTALL_SUCCESS=true
|
||
else
|
||
print_warning "Vulkan build failed - checking for NVIDIA GPU to try CUDA..."
|
||
fi
|
||
fi
|
||
|
||
# If Vulkan failed or not available, try CUDA for NVIDIA GPUs
|
||
if [[ "$GPU_INSTALL_SUCCESS" != "true" && "$HAS_NVIDIA_GPU" == "yes" ]]; then
|
||
print_info "✓ NVIDIA GPU detected: $GPU_NAME"
|
||
print_info " Installing pywhispercpp with CUDA support..."
|
||
GPU_BACKEND="CUDA"
|
||
print_info "Installing pywhispercpp ($GPU_BACKEND backend)..."
|
||
if CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" GGML_CUDA=1 pip install --force-reinstall --no-cache-dir git+https://github.com/absadiki/pywhispercpp --log "$PIP_LOG_FILE" 2>&1; then
|
||
GPU_INSTALL_SUCCESS=true
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# Fall back to CPU version if GPU install failed or no GPU detected
|
||
if [[ "$SKIP_WHISPERCPP_INSTALL" != "true" && "$GPU_INSTALL_SUCCESS" != "true" ]]; then
|
||
if [[ "$GPU_BACKEND" != "CPU" ]]; then
|
||
print_warning "Failed to install pywhispercpp with $GPU_BACKEND support, falling back to CPU version..."
|
||
|
||
# Provide helpful error messages for common issues
|
||
if [[ "$GPU_BACKEND" == "Vulkan" ]]; then
|
||
print_info " To use Vulkan GPU acceleration, please install Vulkan development libraries:"
|
||
print_info " Ubuntu/Debian: sudo apt install libvulkan-dev vulkan-tools glslc || glslang-tools"
|
||
print_info " Fedora: sudo dnf install vulkan-loader-devel vulkan-tools glslang"
|
||
print_info " Arch: sudo pacman -S vulkan-headers vulkan-tools glslang"
|
||
print_info " openSUSE: sudo zypper install vulkan-devel vulkan-tools shaderc"
|
||
elif [[ "$GPU_BACKEND" == "CUDA" ]]; then
|
||
print_info " To use CUDA GPU acceleration, please install CUDA toolkit:"
|
||
print_info " Visit: https://developer.nvidia.com/cuda-downloads"
|
||
fi
|
||
elif [[ "${WHISPERCPP_BACKEND}" != "cpu" ]]; then
|
||
print_info "ℹ No GPU detected - installing CPU-only version"
|
||
print_info " CPU mode is still very fast!"
|
||
fi
|
||
GPU_BACKEND="CPU"
|
||
print_info "Installing pywhispercpp ($GPU_BACKEND backend)..."
|
||
install_cpu_pywhispercpp "$PIP_LOG_FILE" || {
|
||
print_error "Failed to install pywhispercpp"
|
||
return 1
|
||
}
|
||
fi
|
||
|
||
if [[ "$SKIP_WHISPERCPP_INSTALL" == "true" ]]; then
|
||
print_success "pywhispercpp reused from existing installation"
|
||
else
|
||
print_success "pywhispercpp installed with $GPU_BACKEND backend"
|
||
fi
|
||
|
||
if ! is_pywhispercpp_installed; then
|
||
print_warning "pywhispercpp installed but import verification failed."
|
||
print_warning "This can happen when libwhisper.so is installed beside the Python extension without a runtime library path."
|
||
|
||
if [[ "$SKIP_WHISPERCPP_INSTALL" != "true" ]]; then
|
||
print_warning "Trying RPATH-aware CPU pywhispercpp fallback..."
|
||
GPU_BACKEND="CPU"
|
||
if install_cpu_pywhispercpp "$PIP_LOG_FILE"; then
|
||
print_success "CPU pywhispercpp fallback installed"
|
||
else
|
||
print_warning "CPU pywhispercpp fallback installation failed"
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
if ! is_pywhispercpp_installed; then
|
||
print_warning "pywhispercpp is still unavailable; setting VOSK as the default engine so Vocalinux can start."
|
||
print_warning "You can switch back to whisper.cpp from Settings after reinstalling pywhispercpp."
|
||
SELECTED_ENGINE="vosk"
|
||
|
||
local FALLBACK_VOSK_CONFIG="$CONFIG_DIR/config.json"
|
||
if [ ! -f "$FALLBACK_VOSK_CONFIG" ]; then
|
||
mkdir -p "$CONFIG_DIR"
|
||
cat > "$FALLBACK_VOSK_CONFIG" << 'FALLBACK_VOSK_CONFIG'
|
||
{
|
||
"speech_recognition": {
|
||
"engine": "vosk",
|
||
"model_size": "small",
|
||
"vosk_model_size": "small",
|
||
"whisper_model_size": "tiny",
|
||
"whisper_cpp_model_size": "tiny",
|
||
"vad_sensitivity": 3,
|
||
"silence_timeout": 2.0
|
||
},
|
||
"audio": {
|
||
"device_index": null,
|
||
"device_name": null
|
||
},
|
||
"shortcuts": {
|
||
"toggle_recognition": "ctrl+ctrl"
|
||
},
|
||
"ui": {
|
||
"start_minimized": false,
|
||
"show_notifications": true
|
||
},
|
||
"advanced": {
|
||
"debug_logging": false,
|
||
"wayland_mode": false
|
||
}
|
||
}
|
||
FALLBACK_VOSK_CONFIG
|
||
fi
|
||
fi
|
||
echo ""
|
||
}
|
||
|
||
# Function to install Python package with error handling and verification
|
||
install_python_package() {
|
||
# Create a temporary directory for pip logs
|
||
local PIP_LOG_DIR=$(mktemp -d)
|
||
local PIP_LOG_FILE="$PIP_LOG_DIR/pip_log.txt"
|
||
|
||
# Detect GI_TYPELIB_PATH early for cross-distro compatibility
|
||
# This ensures the path is available for both verification and wrapper scripts
|
||
local GI_TYPELIB_DETECTED
|
||
GI_TYPELIB_DETECTED=$(detect_typelib_path)
|
||
print_info "Detected GI_TYPELIB_PATH: $GI_TYPELIB_DETECTED"
|
||
|
||
local WHISPERCPP_ALREADY_INSTALLED=false
|
||
if is_pywhispercpp_installed; then
|
||
WHISPERCPP_ALREADY_INSTALLED=true
|
||
fi
|
||
|
||
# Function to verify package installation
|
||
verify_package_installed() {
|
||
local PKG_NAME="vocalinux"
|
||
# Use venv python and set GI_TYPELIB_PATH for PyGObject
|
||
# Use the detected path for cross-distro compatibility
|
||
GI_TYPELIB_PATH="$GI_TYPELIB_DETECTED" "$VENV_DIR/bin/python" -c "import $PKG_NAME" 2>/dev/null
|
||
return $?
|
||
}
|
||
|
||
# Silero/ONNX Runtime gives much better speech/silence decisions, but
|
||
# onnxruntime wheels are not guaranteed for every Python/platform combo.
|
||
# Install it opportunistically so fresh installs and rerun-updates get the
|
||
# neural VAD when available without blocking the amplitude fallback path.
|
||
install_vad_support() {
|
||
local PIP_LOG_FILE="$1"
|
||
local EDITABLE_MODE="${2:-no}"
|
||
|
||
print_info "Installing neural VAD support (Silero / ONNX Runtime)..."
|
||
local VAD_INSTALL_SUCCESS=false
|
||
if [[ "$EDITABLE_MODE" == "yes" ]]; then
|
||
pip install -e ".[vad]" --log "$PIP_LOG_FILE" && VAD_INSTALL_SUCCESS=true
|
||
else
|
||
pip install ".[vad]" --log "$PIP_LOG_FILE" && VAD_INSTALL_SUCCESS=true
|
||
fi
|
||
|
||
if [[ "$VAD_INSTALL_SUCCESS" == "true" ]]; then
|
||
if "$VENV_DIR/bin/python" - <<'PY' 2>/dev/null
|
||
from vocalinux.speech_recognition.silero_vad import is_silero_available
|
||
raise SystemExit(0 if is_silero_available() else 1)
|
||
PY
|
||
then
|
||
print_success "Neural VAD support installed and verified successfully."
|
||
else
|
||
print_warning "Neural VAD dependencies installed, but Silero VAD could not be verified."
|
||
print_warning "Vocalinux will use amplitude-based VAD until this is resolved."
|
||
fi
|
||
else
|
||
print_warning "Failed to install neural VAD support."
|
||
print_warning "Vocalinux will still work using amplitude-based VAD."
|
||
print_warning "Check the pip log for details: $PIP_LOG_FILE"
|
||
fi
|
||
}
|
||
|
||
if [[ "$DEV_MODE" == "yes" ]]; then
|
||
print_info "Installing Vocalinux in development mode..."
|
||
|
||
# Install in development mode with logging
|
||
pip install -e . --log "$PIP_LOG_FILE" || {
|
||
print_error "Failed to install Vocalinux in development mode."
|
||
print_error "Check the pip log for details: $PIP_LOG_FILE"
|
||
return 1
|
||
}
|
||
|
||
# Install test dependencies
|
||
print_info "Installing test dependencies..."
|
||
pip install pytest pytest-mock pytest-cov --log "$PIP_LOG_FILE" || {
|
||
print_warning "Failed to install some test dependencies. Tests may not run correctly."
|
||
}
|
||
|
||
# Install all optional dependencies for development
|
||
print_info "Installing all optional dependencies for development..."
|
||
pip install -e ".[whisper,dev]" --log "$PIP_LOG_FILE" || {
|
||
print_warning "Failed to install some optional dependencies."
|
||
print_warning "Some features may not work correctly."
|
||
}
|
||
|
||
install_vad_support "$PIP_LOG_FILE" yes
|
||
|
||
if [[ "${SELECTED_ENGINE:-whisper_cpp}" == "whisper_cpp" ]]; then
|
||
install_whispercpp_with_gpu_support "$PIP_LOG_FILE"
|
||
fi
|
||
else
|
||
print_info "Installing Vocalinux..."
|
||
|
||
# Install the package with logging (includes pywhispercpp by default)
|
||
pip install . --log "$PIP_LOG_FILE" || {
|
||
print_error "Failed to install Vocalinux."
|
||
print_error "Check the pip log for details: $PIP_LOG_FILE"
|
||
return 1
|
||
}
|
||
|
||
install_vad_support "$PIP_LOG_FILE"
|
||
|
||
# Engine installation logic:
|
||
# - SELECTED_ENGINE is set by interactive mode or --engine flag
|
||
# - WHISPERCPP_BACKEND is set by interactive mode ("gpu" or "cpu")
|
||
# - Default is whisper_cpp for best performance
|
||
case "${SELECTED_ENGINE:-whisper_cpp}" in
|
||
whisper_cpp)
|
||
install_whispercpp_with_gpu_support "$PIP_LOG_FILE"
|
||
;;
|
||
|
||
whisper)
|
||
print_info "Installing Whisper (OpenAI) with PyTorch..."
|
||
print_info "Note: This engine requires NVIDIA GPU for acceleration"
|
||
print_info " For AMD/Intel GPUs, whisper.cpp is recommended"
|
||
|
||
local WHISPER_INSTALL_SUCCESS=false
|
||
|
||
# Install PyTorch and whisper
|
||
print_info "Installing PyTorch..."
|
||
if pip install torch torchaudio --index-url https://download.pytorch.org/whl/cpu --log "$PIP_LOG_FILE" 2>&1; then
|
||
print_success "PyTorch installed successfully"
|
||
|
||
print_info "Installing openai-whisper..."
|
||
if pip install openai-whisper --log "$PIP_LOG_FILE" 2>&1; then
|
||
# Verify the installation by importing the module
|
||
if "$VENV_DIR/bin/python" -c "import whisper" 2>/dev/null; then
|
||
WHISPER_INSTALL_SUCCESS=true
|
||
print_success "Whisper installed and verified successfully"
|
||
else
|
||
print_error "Whisper package installed but import failed"
|
||
fi
|
||
else
|
||
print_error "Failed to install openai-whisper package"
|
||
fi
|
||
else
|
||
print_error "Failed to install PyTorch"
|
||
fi
|
||
|
||
if [[ "$WHISPER_INSTALL_SUCCESS" == "true" ]]; then
|
||
# Create config with whisper as default
|
||
local WHISPER_CONFIG="$CONFIG_DIR/config.json"
|
||
if [ ! -f "$WHISPER_CONFIG" ]; then
|
||
mkdir -p "$CONFIG_DIR"
|
||
cat > "$WHISPER_CONFIG" << 'WHISPER_CONFIG'
|
||
{
|
||
"speech_recognition": {
|
||
"engine": "whisper",
|
||
"model_size": "tiny",
|
||
"vosk_model_size": "small",
|
||
"whisper_model_size": "tiny",
|
||
"whisper_cpp_model_size": "tiny",
|
||
"vad_sensitivity": 3,
|
||
"silence_timeout": 2.0
|
||
},
|
||
"audio": {
|
||
"device_index": null,
|
||
"device_name": null
|
||
},
|
||
"shortcuts": {
|
||
"toggle_recognition": "ctrl+ctrl"
|
||
},
|
||
"ui": {
|
||
"start_minimized": false,
|
||
"show_notifications": true
|
||
},
|
||
"advanced": {
|
||
"debug_logging": false,
|
||
"wayland_mode": false
|
||
}
|
||
}
|
||
WHISPER_CONFIG
|
||
fi
|
||
else
|
||
print_warning "Failed to install Whisper (OpenAI)"
|
||
print_warning "Falling back to whisper.cpp (recommended engine)"
|
||
print_info ""
|
||
print_info "The Whisper (OpenAI) engine installation failed."
|
||
print_info "whisper.cpp will be installed instead, which is:"
|
||
print_info " - Faster and more accurate"
|
||
print_info " - Works with any GPU (NVIDIA, AMD, Intel)"
|
||
print_info " - Uses Vulkan for GPU acceleration"
|
||
print_info ""
|
||
|
||
# Fall back to whisper.cpp installation
|
||
install_cpu_pywhispercpp "$PIP_LOG_FILE" || {
|
||
print_error "Failed to install pywhispercpp fallback"
|
||
print_error "Please try installing manually: pip install pywhispercpp"
|
||
return 1
|
||
}
|
||
print_success "Installed whisper.cpp as fallback"
|
||
|
||
# Create config with whisper_cpp as default
|
||
local FALLBACK_CONFIG="$CONFIG_DIR/config.json"
|
||
if [ ! -f "$FALLBACK_CONFIG" ]; then
|
||
mkdir -p "$CONFIG_DIR"
|
||
cat > "$FALLBACK_CONFIG" << 'FALLBACK_CONFIG'
|
||
{
|
||
"speech_recognition": {
|
||
"engine": "whisper_cpp",
|
||
"model_size": "tiny",
|
||
"vosk_model_size": "small",
|
||
"whisper_model_size": "tiny",
|
||
"whisper_cpp_model_size": "tiny",
|
||
"vad_sensitivity": 3,
|
||
"silence_timeout": 2.0
|
||
},
|
||
"audio": {
|
||
"device_index": null,
|
||
"device_name": null
|
||
},
|
||
"shortcuts": {
|
||
"toggle_recognition": "ctrl+ctrl"
|
||
},
|
||
"ui": {
|
||
"start_minimized": false,
|
||
"show_notifications": true
|
||
},
|
||
"advanced": {
|
||
"debug_logging": false,
|
||
"wayland_mode": false
|
||
}
|
||
}
|
||
FALLBACK_CONFIG
|
||
fi
|
||
fi
|
||
;;
|
||
|
||
vosk)
|
||
print_info "Installing VOSK (lightweight option)..."
|
||
print_info "VOSK is fast and works well on older systems."
|
||
|
||
# Create config with vosk as default
|
||
local VOSK_CONFIG_FILE="$CONFIG_DIR/config.json"
|
||
if [ ! -f "$VOSK_CONFIG_FILE" ]; then
|
||
mkdir -p "$CONFIG_DIR"
|
||
cat > "$VOSK_CONFIG_FILE" << 'VOSK_CONFIG'
|
||
{
|
||
"speech_recognition": {
|
||
"engine": "vosk",
|
||
"model_size": "small",
|
||
"vosk_model_size": "small",
|
||
"whisper_model_size": "tiny",
|
||
"whisper_cpp_model_size": "tiny",
|
||
"vad_sensitivity": 3,
|
||
"silence_timeout": 2.0
|
||
},
|
||
"audio": {
|
||
"device_index": null,
|
||
"device_name": null
|
||
},
|
||
"shortcuts": {
|
||
"toggle_recognition": "ctrl+ctrl"
|
||
},
|
||
"ui": {
|
||
"start_minimized": false,
|
||
"show_notifications": true
|
||
},
|
||
"advanced": {
|
||
"debug_logging": false,
|
||
"wayland_mode": false
|
||
}
|
||
}
|
||
VOSK_CONFIG
|
||
fi
|
||
;;
|
||
|
||
remote_api)
|
||
print_info "Setting up Remote API engine..."
|
||
print_info ""
|
||
print_info "╔════════════════════════════════════════════════════════╗"
|
||
print_info "║ Setting up REMOTE API Engine ║"
|
||
print_info "╠════════════════════════════════════════════════════════╣"
|
||
print_info "║ • Offloads speech recognition to a remote server ║"
|
||
print_info "║ • Ideal for laptops without GPU ║"
|
||
print_info "║ • Supports whisper.cpp server & OpenAI APIs ║"
|
||
print_info "║ • Requires: a server running on your network ║"
|
||
print_info "╚════════════════════════════════════════════════════════╝"
|
||
print_info ""
|
||
|
||
# Ensure requests library is installed
|
||
print_info "Installing requests library..."
|
||
pip install requests --log "$PIP_LOG_FILE" || {
|
||
print_error "Failed to install requests library"
|
||
return 1
|
||
}
|
||
print_success "requests library installed"
|
||
|
||
# URL was collected upfront by run_interactive_install
|
||
# (or left blank in auto/non-interactive mode).
|
||
local REMOTE_API_URL="${REMOTE_API_URL:-}"
|
||
|
||
# Create configuration file
|
||
local REMOTE_CONFIG_FILE="$CONFIG_DIR/config.json"
|
||
if [ ! -f "$REMOTE_CONFIG_FILE" ]; then
|
||
mkdir -p "$CONFIG_DIR"
|
||
cat > "$REMOTE_CONFIG_FILE" << REMOTE_CONFIG
|
||
{
|
||
"speech_recognition": {
|
||
"engine": "remote_api",
|
||
"model_size": "small",
|
||
"vosk_model_size": "small",
|
||
"whisper_model_size": "tiny",
|
||
"whisper_cpp_model_size": "tiny",
|
||
"remote_api_url": "${REMOTE_API_URL}",
|
||
"remote_api_key": "",
|
||
"vad_sensitivity": 3,
|
||
"silence_timeout": 2.0
|
||
},
|
||
"audio": {
|
||
"device_index": null,
|
||
"device_name": null
|
||
},
|
||
"shortcuts": {
|
||
"toggle_recognition": "ctrl+ctrl"
|
||
},
|
||
"ui": {
|
||
"start_minimized": false,
|
||
"show_notifications": true
|
||
},
|
||
"advanced": {
|
||
"debug_logging": false,
|
||
"wayland_mode": false
|
||
}
|
||
}
|
||
REMOTE_CONFIG
|
||
fi
|
||
|
||
if [ -n "$REMOTE_API_URL" ]; then
|
||
print_success "Remote API configured with server: $REMOTE_API_URL"
|
||
else
|
||
print_warning "No server URL configured. You can set it later in Settings."
|
||
fi
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
# Verify installation
|
||
if verify_package_installed; then
|
||
print_success "Vocalinux package installed successfully!"
|
||
# Clean up log file if installation was successful
|
||
rm -rf "$PIP_LOG_DIR"
|
||
|
||
# GI_TYPELIB_PATH was already detected at the start of install_python_package
|
||
|
||
# Create wrapper scripts in ~/.local/bin for easy access
|
||
mkdir -p "$HOME/.local/bin"
|
||
|
||
# Create vocalinux wrapper script
|
||
# Uses 'sg input' to run with input group for keyboard shortcuts on Wayland
|
||
# This allows shortcuts to work without logging out after installation
|
||
cat > "$HOME/.local/bin/vocalinux" << WRAPPER_EOF
|
||
#!/bin/bash
|
||
# Wrapper script for Vocalinux that sets required environment variables
|
||
# and applies the 'input' group for keyboard shortcuts on Wayland
|
||
export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED
|
||
PYWHISPERCPP_LIBRARY_PATH=""
|
||
PY_SITE_PATHS=\$("$VENV_DIR/bin/python" - <<'PY' 2>/dev/null
|
||
import sysconfig
|
||
|
||
paths = []
|
||
for key in ("platlib", "purelib"):
|
||
path = sysconfig.get_paths().get(key)
|
||
if path and path not in paths:
|
||
paths.append(path)
|
||
print(" ".join(paths))
|
||
PY
|
||
)
|
||
for PY_SITE in \$PY_SITE_PATHS; do
|
||
for PY_LIB_DIR in "\$PY_SITE/pywhispercpp.libs" "\$PY_SITE/pywhispercpp/.libs" "\$PY_SITE/pywhispercpp/lib"; do
|
||
if [ -d "\$PY_LIB_DIR" ] && { ls "\$PY_LIB_DIR"/libwhisper*.so* >/dev/null 2>&1 || ls "\$PY_LIB_DIR"/libggml*.so* >/dev/null 2>&1; }; then
|
||
if [ -z "\$PYWHISPERCPP_LIBRARY_PATH" ]; then
|
||
PYWHISPERCPP_LIBRARY_PATH="\$PY_LIB_DIR"
|
||
else
|
||
PYWHISPERCPP_LIBRARY_PATH="\$PYWHISPERCPP_LIBRARY_PATH:\$PY_LIB_DIR"
|
||
fi
|
||
fi
|
||
done
|
||
done
|
||
if [ -n "\$PYWHISPERCPP_LIBRARY_PATH" ]; then
|
||
export LD_LIBRARY_PATH="\$PYWHISPERCPP_LIBRARY_PATH\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}"
|
||
fi
|
||
|
||
# Check if user is in input group but current session doesn't have it
|
||
if grep -q "^input:.*\b\$(whoami)\b" /etc/group 2>/dev/null && ! groups | grep -q '\binput\b'; then
|
||
# Use sg to run with input group without requiring logout
|
||
exec sg input -c "$VENV_DIR/bin/vocalinux \$*"
|
||
else
|
||
exec "$VENV_DIR/bin/vocalinux" "\$@"
|
||
fi
|
||
WRAPPER_EOF
|
||
chmod +x "$HOME/.local/bin/vocalinux"
|
||
print_info "Created wrapper: ~/.local/bin/vocalinux"
|
||
|
||
# Create vocalinux-gui wrapper script
|
||
cat > "$HOME/.local/bin/vocalinux-gui" << WRAPPER_EOF
|
||
#!/bin/bash
|
||
# Wrapper script for Vocalinux GUI that sets required environment variables
|
||
# and applies the 'input' group for keyboard shortcuts on Wayland
|
||
export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED
|
||
PYWHISPERCPP_LIBRARY_PATH=""
|
||
PY_SITE_PATHS=\$("$VENV_DIR/bin/python" - <<'PY' 2>/dev/null
|
||
import sysconfig
|
||
|
||
paths = []
|
||
for key in ("platlib", "purelib"):
|
||
path = sysconfig.get_paths().get(key)
|
||
if path and path not in paths:
|
||
paths.append(path)
|
||
print(" ".join(paths))
|
||
PY
|
||
)
|
||
for PY_SITE in \$PY_SITE_PATHS; do
|
||
for PY_LIB_DIR in "\$PY_SITE/pywhispercpp.libs" "\$PY_SITE/pywhispercpp/.libs" "\$PY_SITE/pywhispercpp/lib"; do
|
||
if [ -d "\$PY_LIB_DIR" ] && { ls "\$PY_LIB_DIR"/libwhisper*.so* >/dev/null 2>&1 || ls "\$PY_LIB_DIR"/libggml*.so* >/dev/null 2>&1; }; then
|
||
if [ -z "\$PYWHISPERCPP_LIBRARY_PATH" ]; then
|
||
PYWHISPERCPP_LIBRARY_PATH="\$PY_LIB_DIR"
|
||
else
|
||
PYWHISPERCPP_LIBRARY_PATH="\$PYWHISPERCPP_LIBRARY_PATH:\$PY_LIB_DIR"
|
||
fi
|
||
fi
|
||
done
|
||
done
|
||
if [ -n "\$PYWHISPERCPP_LIBRARY_PATH" ]; then
|
||
export LD_LIBRARY_PATH="\$PYWHISPERCPP_LIBRARY_PATH\${LD_LIBRARY_PATH:+:\$LD_LIBRARY_PATH}"
|
||
fi
|
||
|
||
# Check if user is in input group but current session doesn't have it
|
||
if grep -q "^input:.*\b\$(whoami)\b" /etc/group 2>/dev/null && ! groups | grep -q '\binput\b'; then
|
||
# Use sg to run with input group without requiring logout
|
||
exec sg input -c "$VENV_DIR/bin/vocalinux-gui \$*"
|
||
else
|
||
exec "$VENV_DIR/bin/vocalinux-gui" "\$@"
|
||
fi
|
||
WRAPPER_EOF
|
||
chmod +x "$HOME/.local/bin/vocalinux-gui"
|
||
print_info "Created wrapper: ~/.local/bin/vocalinux-gui"
|
||
|
||
# Check if ~/.local/bin is in PATH
|
||
if [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then
|
||
print_warning "~/.local/bin is not in your PATH"
|
||
print_info "Add this line to your ~/.bashrc or ~/.zshrc:"
|
||
print_info ' export PATH="$HOME/.local/bin:$PATH"'
|
||
fi
|
||
|
||
return 0
|
||
else
|
||
print_error "Vocalinux package installation verification failed."
|
||
print_error "Check the pip log for details: $PIP_LOG_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Install Python package
|
||
if ! install_python_package; then
|
||
print_error "Failed to install Vocalinux package. Installation cannot continue."
|
||
exit 1
|
||
fi
|
||
|
||
# Function to download and install Whisper tiny model
|
||
install_whisper_model() {
|
||
print_info "Installing Whisper tiny model (~75MB)..."
|
||
|
||
# Create whisper models directory
|
||
local WHISPER_DIR="$DATA_DIR/models/whisper"
|
||
mkdir -p "$WHISPER_DIR"
|
||
|
||
# Whisper tiny model URL and path
|
||
local TINY_MODEL_URL="https://openaipublic.azureedge.net/main/whisper/models/65147644a518d12f04e32d6f3b26facc3f8dd46e5390956a9424a650c0ce22b9/tiny.pt"
|
||
local TINY_MODEL_PATH="$WHISPER_DIR/tiny.pt"
|
||
|
||
# Check if model already exists
|
||
if [ -f "$TINY_MODEL_PATH" ]; then
|
||
print_info "Whisper tiny model already exists at $TINY_MODEL_PATH"
|
||
return 0
|
||
fi
|
||
|
||
# Check internet connectivity
|
||
if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then
|
||
print_warning "Neither wget nor curl found. Cannot download Whisper model."
|
||
print_warning "Model will be downloaded on first application run."
|
||
return 1
|
||
fi
|
||
|
||
# Test internet connectivity
|
||
if ! ping -c 1 google.com >/dev/null 2>&1; then
|
||
print_warning "No internet connection detected."
|
||
print_warning "Whisper model will be downloaded on first application run."
|
||
return 1
|
||
fi
|
||
|
||
print_info "Downloading Whisper tiny model..."
|
||
print_info "This may take a few minutes depending on your internet connection."
|
||
|
||
local TEMP_FILE="$TINY_MODEL_PATH.tmp"
|
||
|
||
# Download the model
|
||
if command -v wget >/dev/null 2>&1; then
|
||
if ! wget --progress=bar:force:noscroll -O "$TEMP_FILE" "$TINY_MODEL_URL" 2>&1; then
|
||
print_error "Failed to download Whisper model with wget"
|
||
rm -f "$TEMP_FILE"
|
||
return 1
|
||
fi
|
||
elif command -v curl >/dev/null 2>&1; then
|
||
if ! curl -L --progress-bar -o "$TEMP_FILE" "$TINY_MODEL_URL"; then
|
||
print_error "Failed to download Whisper model with curl"
|
||
rm -f "$TEMP_FILE"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Verify download
|
||
if [ ! -f "$TEMP_FILE" ] || [ ! -s "$TEMP_FILE" ]; then
|
||
print_error "Downloaded model file is empty or missing"
|
||
rm -f "$TEMP_FILE"
|
||
return 1
|
||
fi
|
||
|
||
# Move to final location
|
||
mv "$TEMP_FILE" "$TINY_MODEL_PATH"
|
||
|
||
# Verify the model file
|
||
if [ -f "$TINY_MODEL_PATH" ]; then
|
||
local MODEL_SIZE=$(du -h "$TINY_MODEL_PATH" | cut -f1)
|
||
print_success "Whisper tiny model installed successfully ($MODEL_SIZE)"
|
||
|
||
# Create a marker file to indicate this model was pre-installed
|
||
echo "$(date)" > "$WHISPER_DIR/.vocalinux_preinstalled"
|
||
|
||
return 0
|
||
else
|
||
print_error "Whisper model installation failed"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Function to download and install VOSK models
|
||
install_vosk_models() {
|
||
print_info "Installing VOSK speech recognition models..."
|
||
|
||
# Create models directory
|
||
local MODELS_DIR="$DATA_DIR/models"
|
||
mkdir -p "$MODELS_DIR"
|
||
|
||
# Define model information
|
||
local SMALL_MODEL_URL="https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip"
|
||
local SMALL_MODEL_NAME="vosk-model-small-en-us-0.15"
|
||
local SMALL_MODEL_PATH="$MODELS_DIR/$SMALL_MODEL_NAME"
|
||
|
||
# Check if small model already exists
|
||
if [ -d "$SMALL_MODEL_PATH" ]; then
|
||
print_info "Small VOSK model already exists at $SMALL_MODEL_PATH"
|
||
return 0
|
||
fi
|
||
|
||
# Check internet connectivity
|
||
if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then
|
||
print_warning "Neither wget nor curl found. Cannot download VOSK models."
|
||
print_warning "Models will be downloaded on first application run."
|
||
return 1
|
||
fi
|
||
|
||
# Test internet connectivity
|
||
if ! ping -c 1 google.com >/dev/null 2>&1; then
|
||
print_warning "No internet connection detected."
|
||
print_warning "VOSK models will be downloaded on first application run."
|
||
return 1
|
||
fi
|
||
|
||
print_info "Downloading small VOSK model (approximately 40MB)..."
|
||
print_info "This may take a few minutes depending on your internet connection."
|
||
|
||
local TEMP_ZIP="$MODELS_DIR/$(basename $SMALL_MODEL_URL)"
|
||
|
||
# Download the model
|
||
if command -v wget >/dev/null 2>&1; then
|
||
if ! wget --progress=bar:force:noscroll -O "$TEMP_ZIP" "$SMALL_MODEL_URL" 2>&1; then
|
||
print_error "Failed to download VOSK model with wget"
|
||
rm -f "$TEMP_ZIP"
|
||
return 1
|
||
fi
|
||
elif command -v curl >/dev/null 2>&1; then
|
||
if ! curl -L --progress-bar -o "$TEMP_ZIP" "$SMALL_MODEL_URL"; then
|
||
print_error "Failed to download VOSK model with curl"
|
||
rm -f "$TEMP_ZIP"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Verify download
|
||
if [ ! -f "$TEMP_ZIP" ] || [ ! -s "$TEMP_ZIP" ]; then
|
||
print_error "Downloaded model file is empty or missing"
|
||
rm -f "$TEMP_ZIP"
|
||
return 1
|
||
fi
|
||
|
||
print_info "Extracting VOSK model..."
|
||
|
||
# Extract the model
|
||
if command -v unzip >/dev/null 2>&1; then
|
||
if ! unzip -q "$TEMP_ZIP" -d "$MODELS_DIR"; then
|
||
print_error "Failed to extract VOSK model"
|
||
rm -f "$TEMP_ZIP"
|
||
return 1
|
||
fi
|
||
else
|
||
print_error "unzip command not found. Cannot extract VOSK model."
|
||
rm -f "$TEMP_ZIP"
|
||
return 1
|
||
fi
|
||
|
||
# Clean up zip file
|
||
rm -f "$TEMP_ZIP"
|
||
|
||
# Verify extraction
|
||
if [ -d "$SMALL_MODEL_PATH" ]; then
|
||
print_success "VOSK small model installed successfully at $SMALL_MODEL_PATH"
|
||
|
||
# Set proper permissions
|
||
chmod -R 755 "$SMALL_MODEL_PATH"
|
||
|
||
# Create a marker file to indicate this model was pre-installed
|
||
echo "$(date)" > "$SMALL_MODEL_PATH/.vocalinux_preinstalled"
|
||
|
||
return 0
|
||
else
|
||
print_error "VOSK model extraction failed - directory not found"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Function to download and install whisper.cpp tiny model
|
||
install_whispercpp_model() {
|
||
print_info "Installing whisper.cpp tiny model (~39MB)..."
|
||
|
||
# Create whisper.cpp models directory
|
||
local WHISPERCPP_DIR="$DATA_DIR/models/whispercpp"
|
||
mkdir -p "$WHISPERCPP_DIR"
|
||
|
||
# whisper.cpp tiny model URL and path
|
||
local TINY_MODEL_URL="https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin"
|
||
local TINY_MODEL_PATH="$WHISPERCPP_DIR/ggml-tiny.bin"
|
||
|
||
# Check if model already exists
|
||
if [ -f "$TINY_MODEL_PATH" ]; then
|
||
print_info "whisper.cpp tiny model already exists at $TINY_MODEL_PATH"
|
||
return 0
|
||
fi
|
||
|
||
# Check internet connectivity
|
||
if ! command -v wget >/dev/null 2>&1 && ! command -v curl >/dev/null 2>&1; then
|
||
print_warning "Neither wget nor curl found. Cannot download whisper.cpp model."
|
||
print_warning "Model will be downloaded on first application run."
|
||
return 1
|
||
fi
|
||
|
||
# Test internet connectivity
|
||
if ! ping -c 1 google.com >/dev/null 2>&1; then
|
||
print_warning "No internet connection detected."
|
||
print_warning "whisper.cpp model will be downloaded on first application run."
|
||
return 1
|
||
fi
|
||
|
||
print_info "Downloading whisper.cpp tiny model..."
|
||
print_info "This may take a few minutes depending on your internet connection."
|
||
|
||
local TEMP_FILE="$TINY_MODEL_PATH.tmp"
|
||
|
||
# Download the model
|
||
if command -v wget >/dev/null 2>&1; then
|
||
if ! wget --progress=bar:force:noscroll -O "$TEMP_FILE" "$TINY_MODEL_URL" 2>&1; then
|
||
print_error "Failed to download whisper.cpp model with wget"
|
||
rm -f "$TEMP_FILE"
|
||
return 1
|
||
fi
|
||
elif command -v curl >/dev/null 2>&1; then
|
||
if ! curl -L --progress-bar -o "$TEMP_FILE" "$TINY_MODEL_URL"; then
|
||
print_error "Failed to download whisper.cpp model with curl"
|
||
rm -f "$TEMP_FILE"
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
# Verify download
|
||
if [ ! -f "$TEMP_FILE" ] || [ ! -s "$TEMP_FILE" ]; then
|
||
print_error "Downloaded model file is empty or missing"
|
||
rm -f "$TEMP_FILE"
|
||
return 1
|
||
fi
|
||
|
||
# Move to final location
|
||
mv "$TEMP_FILE" "$TINY_MODEL_PATH"
|
||
|
||
# Verify the model file
|
||
if [ -f "$TINY_MODEL_PATH" ]; then
|
||
local MODEL_SIZE=$(du -h "$TINY_MODEL_PATH" | cut -f1)
|
||
print_success "whisper.cpp tiny model installed successfully ($MODEL_SIZE)"
|
||
|
||
# Create a marker file to indicate this model was pre-installed
|
||
echo "$(date)" > "$WHISPERCPP_DIR/.vocalinux_preinstalled"
|
||
|
||
return 0
|
||
else
|
||
print_error "whisper.cpp model installation failed"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Function to install desktop entry with error handling
|
||
install_desktop_entry() {
|
||
print_info "Installing desktop entry..."
|
||
|
||
# Check if desktop entry file exists
|
||
if [ ! -f "vocalinux.desktop" ]; then
|
||
print_error "Desktop entry file not found: vocalinux.desktop"
|
||
return 1
|
||
fi
|
||
|
||
# Create desktop directory if it doesn't exist
|
||
mkdir -p "$DESKTOP_DIR" || {
|
||
print_error "Failed to create desktop directory: $DESKTOP_DIR"
|
||
return 1
|
||
}
|
||
|
||
# Copy desktop entry
|
||
cp vocalinux.desktop "$DESKTOP_DIR/" || {
|
||
print_error "Failed to copy desktop entry to $DESKTOP_DIR"
|
||
return 1
|
||
}
|
||
|
||
# Update the desktop entry to use the wrapper script with GI_TYPELIB_PATH
|
||
WRAPPER_SCRIPT="$HOME/.local/bin/vocalinux-gui"
|
||
if [ ! -f "$WRAPPER_SCRIPT" ]; then
|
||
print_warning "Wrapper script not found at $WRAPPER_SCRIPT"
|
||
print_warning "Desktop entry may not work correctly"
|
||
else
|
||
# Update Exec line to include GI_TYPELIB_PATH for PyGObject
|
||
# Use the detected path for cross-distro compatibility
|
||
sed -i "s|^Exec=vocalinux|Exec=env GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED $WRAPPER_SCRIPT|" "$DESKTOP_DIR/vocalinux.desktop" || {
|
||
print_warning "Failed to update desktop entry path"
|
||
}
|
||
print_info "Updated desktop entry to use wrapper script with GI_TYPELIB_PATH"
|
||
fi
|
||
|
||
# Make desktop entry executable
|
||
chmod +x "$DESKTOP_DIR/vocalinux.desktop" || {
|
||
print_warning "Failed to make desktop entry executable"
|
||
}
|
||
|
||
return 0
|
||
}
|
||
|
||
# Function to install icons with error handling
|
||
install_icons() {
|
||
print_info "Installing application icons..."
|
||
|
||
# Create icon directory if it doesn't exist
|
||
mkdir -p "$ICON_DIR" || {
|
||
print_error "Failed to create icon directory: $ICON_DIR"
|
||
return 1
|
||
}
|
||
|
||
# Check if icons directory exists
|
||
if [ ! -d "resources/icons/scalable" ]; then
|
||
print_warning "Custom icons not found in resources/icons/scalable directory"
|
||
return 1
|
||
fi
|
||
|
||
# List of icons to install
|
||
local ICONS=(
|
||
"vocalinux.svg"
|
||
"vocalinux-microphone.svg"
|
||
"vocalinux-microphone-off.svg"
|
||
"vocalinux-microphone-process.svg"
|
||
)
|
||
|
||
# Install each icon
|
||
local INSTALLED_COUNT=0
|
||
for icon in "${ICONS[@]}"; do
|
||
if [ -f "resources/icons/scalable/$icon" ]; then
|
||
cp "resources/icons/scalable/$icon" "$ICON_DIR/" || {
|
||
print_warning "Failed to copy icon: $icon"
|
||
continue
|
||
}
|
||
((INSTALLED_COUNT++))
|
||
else
|
||
print_warning "Icon not found: resources/icons/scalable/$icon"
|
||
fi
|
||
done
|
||
|
||
if [ "$INSTALLED_COUNT" -eq "${#ICONS[@]}" ]; then
|
||
print_success "Installed all custom Vocalinux icons"
|
||
return 0
|
||
elif [ "$INSTALLED_COUNT" -gt 0 ]; then
|
||
print_warning "Installed $INSTALLED_COUNT/${#ICONS[@]} custom Vocalinux icons"
|
||
return 0
|
||
else
|
||
print_error "Failed to install any icons"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Function to update icon cache and desktop database
|
||
update_icon_cache() {
|
||
print_info "Updating icon cache..."
|
||
|
||
# Check if gtk-update-icon-cache command exists
|
||
if command_exists gtk-update-icon-cache; then
|
||
gtk-update-icon-cache -f -t "${XDG_DATA_HOME:-$HOME/.local/share}/icons/hicolor" 2>/dev/null || {
|
||
print_warning "Failed to update icon cache"
|
||
}
|
||
else
|
||
print_warning "gtk-update-icon-cache command not found, skipping icon cache update"
|
||
fi
|
||
|
||
# Update desktop database so the app appears in application menus immediately
|
||
print_info "Updating desktop database..."
|
||
if command_exists update-desktop-database; then
|
||
update-desktop-database "${XDG_DATA_HOME:-$HOME/.local/share}/applications" 2>/dev/null || {
|
||
print_warning "Failed to update desktop database"
|
||
}
|
||
else
|
||
print_warning "update-desktop-database command not found - app may not appear in menu until next login"
|
||
fi
|
||
}
|
||
|
||
# Function to install resources (icons, sounds) to the virtual environment
|
||
# so the resource_manager can find them at runtime
|
||
install_resources_to_venv() {
|
||
print_info "Installing resources to virtual environment..."
|
||
|
||
# Target directory: $VENV_DIR/share/vocalinux/resources
|
||
local VENV_RESOURCES_DIR="$VENV_DIR/share/vocalinux/resources"
|
||
|
||
# Create directories
|
||
mkdir -p "$VENV_RESOURCES_DIR/icons/scalable" || {
|
||
print_warning "Failed to create venv resources directory"
|
||
return 1
|
||
}
|
||
mkdir -p "$VENV_RESOURCES_DIR/sounds" || {
|
||
print_warning "Failed to create venv sounds directory"
|
||
return 1
|
||
}
|
||
|
||
# Copy icons if available
|
||
if [ -d "resources/icons/scalable" ]; then
|
||
cp resources/icons/scalable/*.svg "$VENV_RESOURCES_DIR/icons/scalable/" 2>/dev/null || {
|
||
print_warning "Failed to copy icons to venv resources"
|
||
}
|
||
fi
|
||
|
||
# Copy sounds if available
|
||
if [ -d "resources/sounds" ]; then
|
||
cp resources/sounds/*.wav "$VENV_RESOURCES_DIR/sounds/" 2>/dev/null || {
|
||
print_warning "Failed to copy sounds to venv resources"
|
||
}
|
||
fi
|
||
|
||
# Verify
|
||
local ICON_COUNT=$(ls "$VENV_RESOURCES_DIR/icons/scalable/"*.svg 2>/dev/null | wc -l)
|
||
local SOUND_COUNT=$(ls "$VENV_RESOURCES_DIR/sounds/"*.wav 2>/dev/null | wc -l)
|
||
|
||
if [ "$ICON_COUNT" -gt 0 ] && [ "$SOUND_COUNT" -gt 0 ]; then
|
||
print_success "Installed resources to venv ($ICON_COUNT icons, $SOUND_COUNT sounds)"
|
||
else
|
||
print_warning "Some resources may be missing from venv ($ICON_COUNT icons, $SOUND_COUNT sounds)"
|
||
fi
|
||
}
|
||
|
||
# Install desktop entry
|
||
install_desktop_entry || print_warning "Desktop entry installation failed"
|
||
|
||
# Install icons
|
||
install_icons || print_warning "Icon installation failed"
|
||
|
||
# Install resources to venv for runtime discovery
|
||
install_resources_to_venv || print_warning "Venv resource installation failed"
|
||
|
||
# Install models based on selected engine
|
||
# whisper.cpp is now the default engine
|
||
if [ "$SKIP_MODELS" = "no" ]; then
|
||
# Check which engines are installed and download appropriate models
|
||
|
||
# Install whisper.cpp model (default engine)
|
||
if is_pywhispercpp_installed; then
|
||
print_info "whisper.cpp is installed - downloading tiny model (default engine)..."
|
||
install_whispercpp_model || print_warning "whisper.cpp model download failed - model will be downloaded on first run"
|
||
fi
|
||
|
||
# Install OpenAI Whisper model if whisper engine is installed
|
||
if "$VENV_DIR/bin/python" -c "import whisper" 2>/dev/null; then
|
||
print_info "Whisper (OpenAI) is installed - downloading tiny model..."
|
||
install_whisper_model || print_warning "Whisper model download failed - model will be downloaded on first run"
|
||
fi
|
||
else
|
||
print_info "Skipping model downloads (--skip-models specified)"
|
||
print_info "Models will be downloaded automatically on first application run"
|
||
fi
|
||
|
||
# Install VOSK models (always useful as fallback)
|
||
if [ "$SKIP_MODELS" = "no" ]; then
|
||
install_vosk_models || print_warning "VOSK model installation failed - models will be downloaded on first run"
|
||
else
|
||
print_info "Skipping VOSK model installation (--skip-models specified)"
|
||
print_info "Models will be downloaded automatically on first application run"
|
||
fi
|
||
|
||
# Update icon cache
|
||
update_icon_cache
|
||
|
||
# Function to run tests with better error handling
|
||
run_tests() {
|
||
print_info "Running tests..."
|
||
|
||
# Check if pytest is installed in the virtual environment
|
||
if ! "$VENV_DIR/bin/python" -c "import pytest" &>/dev/null; then
|
||
print_info "Installing pytest and related packages..."
|
||
pip install pytest pytest-mock pytest-cov || {
|
||
print_error "Failed to install pytest. Cannot run tests."
|
||
return 1
|
||
}
|
||
fi
|
||
|
||
# Create a directory for test results
|
||
local TEST_RESULTS_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/vocalinux/test_results"
|
||
mkdir -p "$TEST_RESULTS_DIR"
|
||
local TEST_RESULTS_FILE="$TEST_RESULTS_DIR/pytest_$(date +%Y%m%d_%H%M%S).xml"
|
||
|
||
print_info "Running tests with pytest..."
|
||
print_info "This may take a few minutes..."
|
||
|
||
# Run the tests with pytest and capture output
|
||
local TEST_OUTPUT_FILE=$(mktemp)
|
||
if pytest -v --junitxml="$TEST_RESULTS_FILE" | tee "$TEST_OUTPUT_FILE"; then
|
||
print_success "All tests passed!"
|
||
print_info "Test results saved to: $TEST_RESULTS_FILE"
|
||
rm -f "$TEST_OUTPUT_FILE"
|
||
return 0
|
||
else
|
||
local FAILED_COUNT=$(grep -c "FAILED" "$TEST_OUTPUT_FILE")
|
||
print_error "$FAILED_COUNT tests failed!"
|
||
print_info "Test results saved to: $TEST_RESULTS_FILE"
|
||
print_info "Check the test output for details."
|
||
rm -f "$TEST_OUTPUT_FILE"
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# Run tests if requested
|
||
if [[ "$RUN_TESTS" == "yes" ]]; then
|
||
if run_tests; then
|
||
print_success "Test suite completed successfully."
|
||
else
|
||
print_warning "Test suite completed with failures."
|
||
print_warning "You can still use the application, but some features might not work as expected."
|
||
fi
|
||
fi
|
||
|
||
# Function to verify the installation
|
||
verify_installation() {
|
||
print_info "Verifying installation..."
|
||
local ISSUES=0
|
||
|
||
# Check if virtual environment exists and is activated
|
||
if [ ! -d "$VENV_DIR" ] || [ ! -f "$VENV_DIR/bin/activate" ]; then
|
||
print_error "Virtual environment not found or incomplete."
|
||
ISSUES=$((ISSUES + 1))
|
||
fi
|
||
|
||
# Check if vocalinux command is available
|
||
if ! command -v vocalinux &>/dev/null && [ ! -f "$VENV_DIR/bin/vocalinux" ]; then
|
||
print_error "Vocalinux command not found."
|
||
ISSUES=$((ISSUES + 1))
|
||
fi
|
||
|
||
# Check if desktop entry is installed
|
||
if [ ! -f "$DESKTOP_DIR/vocalinux.desktop" ]; then
|
||
print_warning "Desktop entry not found. Application may not appear in application menu."
|
||
ISSUES=$((ISSUES + 1))
|
||
fi
|
||
|
||
# Check if icons are installed
|
||
local ICON_COUNT=0
|
||
for icon in vocalinux.svg vocalinux-microphone.svg vocalinux-microphone-off.svg vocalinux-microphone-process.svg; do
|
||
if [ -f "$ICON_DIR/$icon" ]; then
|
||
ICON_COUNT=$((ICON_COUNT + 1))
|
||
fi
|
||
done
|
||
|
||
if [ "$ICON_COUNT" -lt 4 ]; then
|
||
print_warning "Some icons are missing. Application may not display correctly."
|
||
ISSUES=$((ISSUES + 1))
|
||
fi
|
||
|
||
# Check if Python package is importable using venv python
|
||
if ! "$VENV_DIR/bin/python" -c "import vocalinux" &>/dev/null; then
|
||
print_error "Vocalinux Python package cannot be imported."
|
||
ISSUES=$((ISSUES + 1))
|
||
fi
|
||
|
||
# Smoke-test the selected speech engine's native library at install time so that
|
||
# "installed but does not run" failures (e.g. libwhisper.so.1 not found on Debian)
|
||
# are surfaced here with actionable guidance rather than silently at first launch.
|
||
local selected_engine="${SELECTED_ENGINE:-whisper_cpp}"
|
||
if [[ "$selected_engine" == "whisper_cpp" ]]; then
|
||
if ! is_pywhispercpp_installed; then
|
||
print_error "pywhispercpp (whisper.cpp engine) installed but cannot be imported at runtime."
|
||
print_error "This usually means libwhisper.so.1 is missing or not on the library path."
|
||
print_error ""
|
||
print_error "Diagnostic steps:"
|
||
print_error " 1. Check for unresolved symbols:"
|
||
print_error " ldd \$(find $VENV_DIR -name '*.so' -path '*/pywhispercpp*' 2>/dev/null | head -1) 2>/dev/null | grep 'not found'"
|
||
print_error " 2. Re-run the installer with: --rebuild-whispercpp"
|
||
print_error " 3. Or switch to VOSK (no native build needed): --engine=vosk"
|
||
ISSUES=$((ISSUES + 1))
|
||
else
|
||
print_success "pywhispercpp (whisper.cpp) import verified successfully."
|
||
fi
|
||
fi
|
||
|
||
# Return the number of issues found
|
||
return $ISSUES
|
||
}
|
||
|
||
# Function to print beautiful welcome message
|
||
print_welcome_message() {
|
||
local ISSUES=$1
|
||
|
||
# ASCII art header
|
||
cat << 'EOF'
|
||
|
||
▗▖ ▗▖ ▗▄▖ ▗▄▄▖ ▗▄▖ ▗▖ ▗▄▄▄▖▗▖ ▗▖▗▖ ▗▖▗▖ ▗▖
|
||
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▌ ▐▌▐▌ █ ▐▛▚▖▐▌▐▌ ▐▌ ▝▚▞▘
|
||
▐▌ ▐▌▐▌ ▐▌▐▌ ▐▛▀▜▌▐▌ █ ▐▌ ▝▜▌▐▌ ▐▌ ▐▌
|
||
▝▚▞▘ ▝▚▄▞▘▝▚▄▄▖▐▌ ▐▌▐▙▄▄▖▗▄█▄▖▐▌ ▐▌▝▚▄▞▘▗▞▘▝▚▖
|
||
|
||
✓ Installation Complete!
|
||
|
||
EOF
|
||
|
||
# Success or warning message
|
||
if [ "$ISSUES" -eq 0 ]; then
|
||
print_success "Vocalinux has been installed successfully!"
|
||
else
|
||
print_warning "Installation complete with $ISSUES minor issue(s)"
|
||
print_warning "The application should still work normally."
|
||
fi
|
||
|
||
# Get engine info for display
|
||
local ENGINE_INFO="${SELECTED_ENGINE:-whisper_cpp}"
|
||
local ENGINE_DISPLAY_NAME=""
|
||
local BACKEND_INFO=""
|
||
|
||
case "$ENGINE_INFO" in
|
||
whisper_cpp)
|
||
ENGINE_DISPLAY_NAME="Whisper.cpp"
|
||
if [[ "${WHISPERCPP_BACKEND}" == "gpu" ]]; then
|
||
BACKEND_INFO="GPU Accelerated"
|
||
else
|
||
BACKEND_INFO="CPU"
|
||
fi
|
||
;;
|
||
whisper)
|
||
ENGINE_DISPLAY_NAME="Whisper (OpenAI)"
|
||
BACKEND_INFO="PyTorch/CUDA"
|
||
;;
|
||
vosk)
|
||
ENGINE_DISPLAY_NAME="VOSK"
|
||
BACKEND_INFO="Lightweight"
|
||
;;
|
||
remote_api)
|
||
ENGINE_DISPLAY_NAME="Remote API"
|
||
BACKEND_INFO="Network (offloaded to remote server)"
|
||
;;
|
||
esac
|
||
|
||
echo ""
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 📦 What Was Installed"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo " Application: Vocalinux (voice dictation for Linux)"
|
||
echo " Engine: $ENGINE_DISPLAY_NAME"
|
||
if [[ -n "$BACKEND_INFO" ]]; then
|
||
echo " Backend: $BACKEND_INFO"
|
||
fi
|
||
echo " Location: ${INSTALL_DIR:-\$HOME/.local/share/vocalinux}"
|
||
echo " Virtual Env: $VENV_DIR"
|
||
echo " Config: $CONFIG_DIR"
|
||
echo ""
|
||
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 🚀 Getting Started"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo "1. Launch Vocalinux"
|
||
echo " • From app menu: Look for 'Vocalinux'"
|
||
echo " • From terminal: Run 'vocalinux' command"
|
||
echo ""
|
||
echo "2. Find the icon in your system tray (top bar)"
|
||
echo " • Click for settings and status"
|
||
echo " • Right-click for menu options"
|
||
echo ""
|
||
echo "3. Start dictating!"
|
||
echo -e " \e[1mDouble-tap Ctrl\e[0m anywhere to toggle recording"
|
||
echo ""
|
||
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 🎤 Testing Your Setup"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo "1. Open any text editor (gedit, VS Code, LibreOffice, etc.)"
|
||
echo "2. Double-tap Ctrl to start recording"
|
||
echo "3. Say: 'Hello world period'"
|
||
echo "4. Double-tap Ctrl to stop"
|
||
echo "5. You should see: 'Hello world.'"
|
||
echo ""
|
||
echo "💡 Voice commands: 'period' 'comma' 'new line' 'delete that'"
|
||
echo ""
|
||
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 🔧 Managing Vocalinux"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo "Commands:"
|
||
echo " vocalinux Start the application"
|
||
echo " vocalinux --debug Start with debug logging"
|
||
echo " vocalinux-gui Open settings GUI"
|
||
echo ""
|
||
echo "To activate the virtual environment:"
|
||
echo " source ${ACTIVATION_SCRIPT:-activate-vocalinux.sh}"
|
||
echo ""
|
||
echo "To uninstall:"
|
||
echo " ./uninstall.sh"
|
||
echo ""
|
||
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 📚 Need Help?"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo "• Issues & Bugs: https://github.com/jatinkrmalik/vocalinux/issues"
|
||
echo "• Documentation: https://github.com/jatinkrmalik/vocalinux"
|
||
echo "• Star on GitHub: ⭐ https://github.com/jatinkrmalik/vocalinux"
|
||
echo ""
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo -e " \e[1m\e[32m✨ Happy Dictating! ✨\e[0m"
|
||
echo ""
|
||
|
||
# Installation details (optional, for debugging)
|
||
if [[ "$VERBOSE" == "yes" ]]; then
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo " 🔍 Installation Details (Debug Mode)"
|
||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||
echo ""
|
||
echo "Virtual environment: $VENV_DIR"
|
||
echo "Desktop entry: $DESKTOP_DIR/vocalinux.desktop"
|
||
echo "Configuration: $CONFIG_DIR"
|
||
echo "Data directory: $DATA_DIR"
|
||
echo "Wrapper script: $HOME/.local/bin/vocalinux"
|
||
echo ""
|
||
fi
|
||
}
|
||
|
||
# Verify the installation
|
||
verify_installation
|
||
INSTALL_ISSUES=$?
|
||
|
||
# Print welcome message
|
||
print_welcome_message $INSTALL_ISSUES
|
||
|
||
print_success "Installation process completed!"
|