feat: Alpha Launch v0.2.0 - Complete UX Overhaul 🚀🎤 (#39)

* feat: prepare for v0.2.0-alpha release

* feat: prepare for v0.2.0-alpha release

* chore: remove CHANGELOG.md, use GitHub Releases instead

* refactor: use existing About dialog instead of Settings section

* style: enhance About dialog UI readability

* style: fix formatting with black and isort

* feat: show VAD and silence timeout settings for Whisper engine

- Renamed 'VOSK Settings' to 'Recognition Settings'
- VAD Sensitivity and Silence Timeout now apply to both engines
- Added tooltips explaining each setting
- VOSK-specific model info only shown when VOSK selected

* fix: resolve overlapping UI elements in settings dialog

* fix: save VAD and silence timeout settings for both engines

Previously these settings were only saved when VOSK was selected.
Now they persist correctly for Whisper as well.

* fix: show download progress when selecting undownloaded model

Previously when auto-applying settings, models would download silently
in the background without showing the progress dialog. Now:
- Download progress dialog appears for undownloaded models
- Model icons update correctly after download completes

* feat: add download progress, speed, ETA and cancel button

- Show real download progress percentage for VOSK models
- Display download speed (MB/s) and estimated time remaining
- Add Cancel button to abort downloads
- Update status during extraction phase
- Whisper downloads still pulse (uses internal downloader)

* fix: defer model download until needed, don't block startup

- App now starts instantly even if models aren't downloaded
- Models are downloaded on-demand when selected in Settings
- If user tries to dictate without a model, plays error sound
- Added model_ready property to check if engine is usable

* fix: improve first-run UX without downloaded models

- Settings dialog now defaults to smallest available model, not saved config
- Only show saved model if it's actually downloaded
- Add desktop notification when trying to dictate without a model
- Prevents auto-downloading large models on first launch

* feat: add download progress tracking for Whisper models

- Implement custom Whisper model download with progress/speed/ETA
- Uses OpenAI's CDN URLs directly instead of whisper.load_model()
- Progress callback works the same as VOSK downloads
- Supports cancellation during download

* fix: enable download progress callback for Whisper models

Progress callback was only being set for VOSK downloads, not Whisper.
Now both engines show real-time progress, speed, and ETA.

* fix: update tests for defer_download and config manager mocking

- test_main: Mock ConfigManager to prevent saved config from overriding test values
- test_settings_dialog: Mock ModelDownloadDialog to prevent GTK init errors
- test_speech_recognition: Use proper MockVoskModule class instead of MagicMock, pass defer_download=False for download test
- test_whisper_support: Pass defer_download=False to trigger model loading in test

* fix: resolve test failures for CI

- test_main: Fix ConfigManager patch path to use source module
- test_settings_dialog: Mock _is_vosk_model_downloaded and _is_whisper_model_downloaded to skip download flow, add missing dialog attributes
- test_speech_recognition: Replace recognizer with properly configured mock that returns valid JSON, fixing test isolation issues when running all tests together

* fix: mock _download_whisper_model in whisper test to avoid requests exception handling issue

* fix: add gir1.2-girepository-2.0 dep for PyGObject on Ubuntu 24.04

- Add missing girepository-2.0 package for newer Ubuntu runners
- Use --no-build-isolation for PyGObject to use system packages

* fix: skip PyGObject pip install, use system python3-gi package

- Use --no-deps for editable install to skip PyGObject
- Install other deps explicitly without PyGObject
- Add python3-gi-cairo for complete GTK support
- Removes problematic girepository-2.0 build requirement

* fix: install PyGObject via pip with proper build deps

- Remove python3-gi/python3-gi-cairo (doesn't work with setup-python)
- Install PyGObject from pip after installing build dependencies
- Simplify venv setup without --system-site-packages hack

* Fix GObject

* fix: set GI_TYPELIB_PATH for pip-installed PyGObject to find AppIndicator3
This commit is contained in:
Jatin K Malik
2025-12-28 15:29:51 -08:00
committed by GitHub
parent 917510d3f4
commit 1dc1d44665
42 changed files with 2222 additions and 1299 deletions
+78 -6
View File
@@ -5,33 +5,105 @@ on:
tags:
- 'v*'
permissions:
contents: write
packages: write
env:
PYTHON_VERSION: '3.10'
jobs:
build-and-release:
name: Build and Create Release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dependencies
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine wheel
pip install build twine wheel setuptools
- name: Extract version from tag
id: get_version
run: |
VERSION=${GITHUB_REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
echo "Building version: $VERSION"
- name: Verify version consistency
run: |
# Get version from Python package
PACKAGE_VERSION=$(python -c "from src.vocalinux.version import __version__; print(__version__)")
TAG_VERSION=${{ steps.get_version.outputs.VERSION }}
echo "Package version: $PACKAGE_VERSION"
echo "Tag version: $TAG_VERSION"
# Allow alpha/beta suffixes in package version
if [[ ! "$PACKAGE_VERSION" == *"$TAG_VERSION"* ]] && [[ ! "$TAG_VERSION" == *"$PACKAGE_VERSION"* ]]; then
echo "Warning: Version mismatch between tag ($TAG_VERSION) and package ($PACKAGE_VERSION)"
fi
- name: Build package
run: |
python -m build
echo "Built packages:"
ls -la dist/
- name: Create Release
id: create_release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: |
dist/*.whl
dist/*.tar.gz
generate_release_notes: true
draft: false
prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }}
body: |
## Installation
### Quick Install (Recommended)
```bash
curl -fsSL https://raw.githubusercontent.com/jatinkrmalik/vocalinux/main/install.sh | bash
```
### From Source
```bash
git clone https://github.com/jatinkrmalik/vocalinux.git
cd vocalinux
./install.sh
```
### From PyPI (if available)
```bash
pip install vocalinux
```
---
See [CHANGELOG](https://github.com/jatinkrmalik/vocalinux/releases) for detailed changes.
# Optional: publish to PyPI when ready
# publish-pypi:
# name: Publish to PyPI
# needs: build-and-release
# runs-on: ubuntu-latest
# if: ${{ !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') }}
# steps:
# - uses: actions/checkout@v4
# - uses: actions/setup-python@v5
# with:
# python-version: ${{ env.PYTHON_VERSION }}
# - run: pip install build twine
# - run: python -m build
# - uses: pypa/gh-action-pypi-publish@release/v1
# with:
# password: ${{ secrets.PYPI_API_TOKEN }}
+55 -173
View File
@@ -1,23 +1,12 @@
name: Vocalinux Unified Pipeline
name: Vocalinux CI
on:
push:
branches: [ main, master ]
paths-ignore:
- '**.md'
- 'docs/**'
pull_request:
branches: [ main, master ]
paths-ignore:
- '**.md'
- 'docs/**'
workflow_dispatch:
# Define path filters for different components
env:
WEB_FILES_FILTER: 'web/**'
PYTHON_FILES_FILTER: 'src/**,tests/**,setup.py'
permissions:
contents: write
pages: write
@@ -25,106 +14,61 @@ permissions:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false
cancel-in-progress: true
jobs:
python-lint:
name: Python Lint
name: 🔍 Lint Python
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# This step checks if Python files were changed
- name: Check for Python file changes
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
python:
- '${{ env.PYTHON_FILES_FILTER }}'
# Skip the rest of the job if no Python files were changed
- name: Skip if no Python files changed
if: steps.changes.outputs.python != 'true' && github.event_name != 'workflow_dispatch' && !contains(github.event.head_commit.message, '[python]')
run: |
echo "No Python files were changed. Skipping Python linting."
exit 0
- name: Set up Python
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
cache-dependency-path: 'setup.py'
- name: Install development dependencies
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
- name: Install linting tools
run: |
python -m pip install --upgrade pip
python -m pip install flake8 black isort
pip install flake8 black isort
- name: Lint with flake8
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
- name: Lint with flake8 (critical errors)
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 src/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings
flake8 src/ tests/ --count --exit-zero --max-complexity=10 --max-line-length=100 --statistics
- name: Check formatting with black
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
run: |
black --check src/ tests/
black --check --diff src/ tests/
- name: Check import sorting with isort
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
run: |
isort --check-only --profile black src/ tests/
isort --check-only --diff --profile black src/ tests/
python-test:
name: Python Test
name: 🧪 Test Python ${{ matrix.python-version }}
runs-on: ubuntu-latest
needs: python-lint
strategy:
fail-fast: false
matrix:
python-version: [3.8]
python-version: ['3.8', '3.10', '3.11']
steps:
- uses: actions/checkout@v4
# This step checks if Python files were changed
- name: Check for Python file changes
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
python:
- '${{ env.PYTHON_FILES_FILTER }}'
# Skip the rest of the job if no Python files were changed
- name: Skip if no Python files changed
if: steps.changes.outputs.python != 'true' && github.event_name != 'workflow_dispatch' && !contains(github.event.head_commit.message, '[python]')
run: |
echo "No Python files were changed. Skipping Python tests."
exit 0
- name: Set up Python ${{ matrix.python-version }}
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
cache-dependency-path: 'setup.py'
- name: Install system dependencies
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
run: |
sudo apt-get update
sudo apt-get install -y \
xdotool \
python3-gi \
gir1.2-appindicator3-0.1 \
libcairo2-dev \
pkg-config \
@@ -135,74 +79,55 @@ jobs:
gir1.2-gtk-3.0 \
portaudio19-dev
- name: Cache apt packages
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
uses: actions/cache@v3
with:
path: /var/cache/apt/archives
key: ${{ runner.os }}-apt-${{ hashFiles('**/apt-packages.txt') }}
restore-keys: |
${{ runner.os }}-apt-
- name: Install GObject introspection dependencies
run: |
sudo apt-get update
# For Ubuntu 24.04+ (or the latest GitHub Actions runner image)
sudo apt-get install -y gir1.2-glib-2.0 libgirepository-2.0-dev pkg-config
# Alternatively, for older Ubuntu versions (like 22.04), you might need:
# sudo apt-get install -y libgirepository1.0-dev pkg-config
- name: Set up virtual environment
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
run: |
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
# Install the package in development mode with dev dependencies
python -m pip install -e ".[dev]"
python -m pip install pytest pytest-cov
python -m pip install --upgrade pip setuptools wheel
# Install PyGObject from pip (with build deps installed above)
pip install PyGObject
pip install -e ".[dev]"
echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV
echo "PATH=$VIRTUAL_ENV/bin:$PATH" >> $GITHUB_ENV
- name: Run pytest suite with coverage
if: steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')
- name: Run tests with coverage
run: |
source venv/bin/activate
# Make sure Python can find the 'src' module
PYTHONPATH=$PWD pytest --junitxml=junit.xml --cov=src tests/
# Set GI_TYPELIB_PATH so pip-installed PyGObject can find system typelibs (AppIndicator3, etc.)
export GI_TYPELIB_PATH=/usr/lib/x86_64-linux-gnu/girepository-1.0:/usr/lib/girepository-1.0
PYTHONPATH=$PWD pytest --junitxml=junit-${{ matrix.python-version }}.xml --cov=src --cov-report=xml tests/
- name: Upload test results to Codecov
if: ${{ !cancelled() && (steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]')) }}
uses: codecov/test-results-action@v1
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.python-version }}
path: junit-${{ matrix.python-version }}.xml
- name: Upload coverage to Codecov
if: matrix.python-version == '3.10'
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.xml
fail_ci_if_error: false
- name: Upload coverage reports to Codecov
if: ${{ steps.changes.outputs.python == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[python]') }}
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
web-validate:
name: Web Validation
web-build:
name: 🌐 Build Website
runs-on: ubuntu-latest
# Use GitHub's path filtering to determine if this job should run
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[web]') }}
if: contains(github.event.head_commit.message, '[web]') || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
# This step checks if web files were changed
- name: Check for web file changes
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
web:
- '${{ env.WEB_FILES_FILTER }}'
# Skip the rest of the job if no web files were changed
- name: Skip if no web files changed
if: steps.changes.outputs.web != 'true' && github.event_name != 'workflow_dispatch' && !contains(github.event.head_commit.message, '[web]')
run: |
echo "No web files were changed. Skipping web validation."
exit 0
# Continue with web validation if files were changed
- name: Setup Node.js
if: steps.changes.outputs.web == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[web]')
uses: actions/setup-node@v4
with:
node-version: '20'
@@ -210,90 +135,47 @@ jobs:
cache-dependency-path: './web/package-lock.json'
- name: Install dependencies
if: steps.changes.outputs.web == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[web]')
working-directory: ./web
run: |
if [ -f "package-lock.json" ]; then
npm ci
else
npm install
fi
run: npm ci || npm install
- name: Lint
if: steps.changes.outputs.web == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[web]')
working-directory: ./web
run: npm run lint || echo "Linting failed but continuing with the build"
run: npm run lint || true
- name: Type check
if: steps.changes.outputs.web == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[web]')
working-directory: ./web
run: npm run test:types || echo "Type checking failed but continuing with the build"
- name: Format check
if: steps.changes.outputs.web == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[web]')
working-directory: ./web
run: npm run format:check || echo "Format checking failed but continuing with the build"
run: npm run test:types || true
- name: Build
if: steps.changes.outputs.web == 'true' || github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, '[web]')
working-directory: ./web
run: npm run build
deploy-website:
name: Deploy Website
name: 🚀 Deploy Website
runs-on: ubuntu-latest
needs: [python-test, web-validate]
# Only deploy on pushes to main branch
if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/main' }}
needs: [python-test, web-build]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && contains(github.event.head_commit.message, '[web]')
steps:
- name: Checkout
uses: actions/checkout@v4
# This step checks if web files were changed
- name: Check for web file changes
uses: dorny/paths-filter@v2
id: changes
with:
filters: |
web:
- '${{ env.WEB_FILES_FILTER }}'
# Skip the rest of the job if no web files were changed
- name: Skip if no web files changed
if: steps.changes.outputs.web != 'true' && !contains(github.event.head_commit.message, '[web]')
run: |
echo "No web files were changed. Skipping website deployment."
exit 0
- uses: actions/checkout@v4
- name: Setup Node.js
if: steps.changes.outputs.web == 'true' || contains(github.event.head_commit.message, '[web]')
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- name: Install dependencies
if: steps.changes.outputs.web == 'true' || contains(github.event.head_commit.message, '[web]')
- name: Install and build
working-directory: ./web
run: |
if [ -f "package-lock.json" ]; then
npm ci
else
npm install
fi
- name: Build website
if: steps.changes.outputs.web == 'true' || contains(github.event.head_commit.message, '[web]')
working-directory: ./web
run: npm run deploy
npm ci || npm install
npm run deploy
- name: Deploy to gh-pages
if: steps.changes.outputs.web == 'true' || contains(github.event.head_commit.message, '[web]')
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./web/out
publish_branch: gh-pages
cname: vocalinux.com
force_orphan: true
force_orphan: true