diff --git a/AGENTS.md b/AGENTS.md index 3853fb7..f7db188 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ Onboarding guide for coding agents working in this repository. ## Project -VoxKit is a PyQt6 desktop application for speech pathology research — a GUI front-end over multiple speech toolkits (alignment, training, transcription). Package lives at `src/voxkit/`. Python 3.11+, managed with `uv`. +VoxKit is a PyQt6 desktop application for speech pathology research; a GUI front-end over multiple speech toolkits (alignment, training, transcription) with a shared storage for interacting with and processing speech datasets. Package lives at `src/voxkit/`. Python 3.11+, managed with `uv`. ## Repository Layout @@ -15,11 +15,18 @@ src/voxkit/ ├── analyzers/ # Dataset metadata extractors (CSV summaries) ├── storage/ # Persistence/CRUD for datasets, models, alignments ├── services/ # External subprocess integrations -└── config/ # App configuration and startup +└── config/ # App configuration loaders (profile-aware) +config/ +├── VERSION # Single source of truth for app version +├── profile.txt # Active profile name +├── app_info.yaml # Legacy fallback metadata +├── pipeline_definitions.yaml # Legacy fallback metadata +└── profiles// # Per-profile yaml configurations tests/ # Pytest suite (unit + GUI via pytest-qt) docs/ # ARCHITECTURE.md, CONTRIBUTING.md, RESEARCH.md -hooks/ # Pre-commit hooks -scripts/ # Dev scripts +hooks/ # Build-time hooks to fix dependency problems in the build +scripts/ # Dev scripts (incl. build.py for PyInstaller) +installer/ # Platform installer scripts (Inno Setup for Windows) main.py # Entry point ``` @@ -33,45 +40,53 @@ Hybrid "unstructured state + signals" PyQt pattern. See `docs/ARCHITECTURE.md` f - **Async work runs in QThread workers** that emit `pyqtSignal` back to views. - **Cross-page state** refreshes via parent window calling `reload()` on tab switch. - **Engines** and **analyzers** each have an abstract base class and a singleton manager for discovery. +- **Config profiles**: `config/profile.txt` selects an active profile under `config/profiles//`. The loader is in (`src/voxkit/config/app_config.py`). ## Setup & Common Commands -Use `invoke` (pyinvoke, tasks defined in `tasks.py`) — do not invoke tools directly unless you need a flag the task doesn't expose. +> **IMPORTANT (read before touching anything):** +> 1. **Run `invoke setup` first, every fresh checkout.** It installs dependencies (`uv sync`), wires up pre-commit hooks, and prepares the local environment. +> 2. **Use `invoke` tasks for everything during development.** Do **not** reach for `pytest`, `ruff`, `mypy`, `pyinstaller`, or `uv run …` directly. The tasks in `tasks.py` set the right flags, paths, and env vars; bypassing them produces results that won't match CI or other contributors. Only call a tool directly if you genuinely need a flag the task doesn't expose, and prefer adding the flag to the task over a one-off workaround. + +Tasks are defined in `tasks.py` (pyinvoke). | Command | Purpose | |---|---| | `invoke setup` | Install deps + pre-commit hooks (run first) | | `invoke dev` | Launch the app in dev mode | +| `invoke watch` | Dev mode with auto-reload on source changes | | `invoke run-tests` | Unit + GUI tests | | `invoke test-coverage` | Coverage for core modules | +| `invoke generate-coverage-badge` | Refresh `coverage.svg` | +| `invoke generate-documentation` | Build pdoc HTML into `docs/` | | `invoke lint` / `invoke lint-check` | Ruff lint (fix / check) | | `invoke format` / `invoke format-check` | Ruff format | | `invoke mypy-check` | Type check | -| `invoke build` | Standalone executable (PyInstaller) | +| `invoke macos-build` / `linux-build` / `windows-build` | Standalone executable (PyInstaller) | | `invoke clean` | Remove build artifacts | +| `invoke fresh-slate` | Remove virtual environment and lock file (Dependency troubleshooting) | | `invoke --list` | Full list | -## Code Standards +## Versioning + +`config/VERSION` is the single source of truth. All consumers read it: + +- `pyproject.toml` via `[tool.setuptools.dynamic] version = {file = ["config/VERSION"]}` +- `src/voxkit/__init__.py` (`__version__` read at import, handles PyInstaller `_MEIPASS`) +- `AppConfig.from_yaml` overrides any YAML `version:` with this file (legacy behavior) +- `installer/windows/VoxKit.iss` reads it via ISPP at compile time -- **Ruff**: line length 100, double quotes, isort-managed imports. Lints: `E`, `F`, `I`, `S` (bandit). Per-file ignores in `pyproject.toml`. -- **Mypy**: Python 3.11, `warn_return_any=true`. `tests/` and `main.py` excluded. -- **Coverage targets**: 70–80% on new business logic in `storage/`, `config/`, `analyzers/`. GUI, engines, and services are deliberately omitted from coverage. -- Pre-commit runs on every commit — don't bypass with `--no-verify`. +To bump the version, edit `config/VERSION` and nothing else. Do not reintroduce hardcoded version strings in `__init__.py`, `app_info.yaml`, or the installer. ## Testing - Framework: `pytest`, with `pytest-qt` for GUI and `pytest-asyncio` for async. - Write tests for new business logic in `storage/`, `config/`, `analyzers/`. GUI components are excluded from coverage metrics but still testable with `pytest-qt` when useful. -- Run `invoke run-tests` before reporting a task complete. For UI changes, also launch `invoke dev` and exercise the feature — type checks don't verify user-facing behavior. - -## Commit & PR Conventions - -Format: `: ` where type ∈ `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`. Keep commits small and logical. See `docs/CONTRIBUTING.md` for the review process. ## Gotchas for Agents -- Two test directories exist at repo root: `tests/` (the real suite, in `pyproject.toml` config) and `test/` (untracked scratch). Put new tests in `tests/`. -- The `pyproject.toml` `name` is still `pypllr-gui` (legacy) but the package is `voxkit`. Don't "fix" this without asking. - `main.py`, `build.py`, `_frozen_patch.py` are excluded from lint/mypy/coverage — they're build/entry shims. +- `src/voxkit/__init__.py` eagerly imports all subpackages (including PyQt6 via `gui`) so pdoc can discover them. `import voxkit` is therefore expensive — fine for the app, painful for scripts that just want `__version__`. Don't "optimize" by removing the imports without checking pdoc output. - Engines and services wrap external binaries; changes there are hard to unit-test and are omitted from coverage by design. - Dependencies pin `torch==2.8.0` and pull several packages from Git SHAs — don't loosen these casually. +- PyInstaller bundles `config/` via `scripts/build.py` (`--add-data`); anything new in `config/` that the runtime needs will ship automatically, but custom paths outside `config/` will not. diff --git a/config/VERSION b/config/VERSION new file mode 100644 index 0000000..267577d --- /dev/null +++ b/config/VERSION @@ -0,0 +1 @@ +0.4.1 diff --git a/config/app_info.yaml b/config/app_info.yaml index 5555953..5b71581 100644 --- a/config/app_info.yaml +++ b/config/app_info.yaml @@ -2,7 +2,7 @@ # This file contains metadata about the application version and purpose app_name: "VoxKit" -version: "0.1.0" +# version is sourced from config/VERSION (single source of truth) description: "AI/ML Research -> Clinical Applications (Speech Pathology)" help_url: "https://voxkit-web.vercel.app/help" @@ -12,17 +12,17 @@ log_backup_count: 3 # number of rotated files to retain # Introduction text displayed to users introduction: | - VoxKit bridges advanced ML alignment tools and clinical speech pathology research. - This toolkit enables rigorous phonetic analysis without requiring deep technical + VoxKit bridges advanced ML alignment tools and clinical speech pathology research. + This toolkit enables rigorous phonetic analysis without requiring deep technical expertise in machine learning or command-line interfaces. - + Core Workflow: 1. Register and analyze your speech datasets 2. Train custom acoustic models or use pretrained engines (MFA, W2TG) 3. Generate phoneme-level forced alignments with timing precision 4. Extract Goodness of Pronunciation (PLLR) scores for clinical assessment 5. Export results with full provenance tracking for reproducible research - + Key Capabilities: - Multiple alignment engines (MFA, Wav2TextGrid, WhisperX in development) - Extensible analyzer system for custom metadata extraction @@ -38,7 +38,7 @@ release_notes: | - Enhanced dataset analyzers with custom metadata extraction - Model management interface with version tracking - Startup routines for automated asset downloads - + Configuration Changes: - Researchers can now modify workflows by editing config/pipeline_definitions.yaml - No code changes required for common workflow adaptations @@ -49,10 +49,10 @@ contact_info: github_issues: "https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop/issues" email_support: "bfrey6@wisc.edu" documentation: "https://voxkit-web.vercel.app/help" - + # Research context research_context: | - VoxKit was developed through collaboration between WISCLab and the Brain Behavior - Analytics Lab to democratize access to state-of-the-art forced alignment tools. - The platform is designed around established speech pathology research methodologies + VoxKit was developed through collaboration between WISCLab and the Brain Behavior + Analytics Lab to democratize access to state-of-the-art forced alignment tools. + The platform is designed around established speech pathology research methodologies rather than generic audio processing workflows. diff --git a/config/profiles/default/app_info.yaml b/config/profiles/default/app_info.yaml index a1882f8..a374395 100644 --- a/config/profiles/default/app_info.yaml +++ b/config/profiles/default/app_info.yaml @@ -2,7 +2,7 @@ # This file contains metadata about the application version and purpose app_name: "VoxKit" -version: "0.1.0" +# version is sourced from config/VERSION (single source of truth) description: "AI/ML Research -> Clinical Applications (Speech Pathology)" help_url: "https://voxkit-web.vercel.app/help" @@ -12,17 +12,17 @@ log_backup_count: 3 # number of rotated files to retain # Introduction text displayed to users introduction: | - VoxKit bridges advanced ML alignment tools and clinical speech pathology research. - This toolkit enables rigorous phonetic analysis without requiring deep technical + VoxKit bridges advanced ML alignment tools and clinical speech pathology research. + This toolkit enables rigorous phonetic analysis without requiring deep technical expertise in machine learning or command-line interfaces. - + Core Workflow: 1. Register and analyze your speech datasets 2. Train custom acoustic models or use pretrained engines (MFA, W2TG) 3. Generate phoneme-level forced alignments with timing precision 4. Extract Goodness of Pronunciation (GOP) scores for clinical assessment 5. Export results with full provenance tracking for reproducible research - + Key Capabilities: - Multiple alignment engines (MFA, Wav2TextGrid, WhisperX in development) - Extensible analyzer system for custom metadata extraction @@ -38,7 +38,7 @@ release_notes: | - Enhanced dataset analyzers with custom metadata extraction - Model management interface with version tracking - Startup routines for automated asset downloads - + Configuration Changes: - Researchers can now modify workflows by editing config/pipeline_definitions.yaml - No code changes required for common workflow adaptations @@ -49,10 +49,10 @@ contact_info: github_issues: "https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop/issues" email_support: "bfrey6@wisc.edu" documentation: "https://voxkit-web.vercel.app/help" - + # Research context research_context: | - VoxKit was developed through collaboration between WISCLab and the Brain Behavior - Analytics Lab to democratize access to state-of-the-art forced alignment tools. - The platform is designed around established speech pathology research methodologies + VoxKit was developed through collaboration between WISCLab and the Brain Behavior + Analytics Lab to democratize access to state-of-the-art forced alignment tools. + The platform is designed around established speech pathology research methodologies rather than generic audio processing workflows. diff --git a/config/profiles/explanatory/app_info.yaml b/config/profiles/explanatory/app_info.yaml index 5555953..5b71581 100644 --- a/config/profiles/explanatory/app_info.yaml +++ b/config/profiles/explanatory/app_info.yaml @@ -2,7 +2,7 @@ # This file contains metadata about the application version and purpose app_name: "VoxKit" -version: "0.1.0" +# version is sourced from config/VERSION (single source of truth) description: "AI/ML Research -> Clinical Applications (Speech Pathology)" help_url: "https://voxkit-web.vercel.app/help" @@ -12,17 +12,17 @@ log_backup_count: 3 # number of rotated files to retain # Introduction text displayed to users introduction: | - VoxKit bridges advanced ML alignment tools and clinical speech pathology research. - This toolkit enables rigorous phonetic analysis without requiring deep technical + VoxKit bridges advanced ML alignment tools and clinical speech pathology research. + This toolkit enables rigorous phonetic analysis without requiring deep technical expertise in machine learning or command-line interfaces. - + Core Workflow: 1. Register and analyze your speech datasets 2. Train custom acoustic models or use pretrained engines (MFA, W2TG) 3. Generate phoneme-level forced alignments with timing precision 4. Extract Goodness of Pronunciation (PLLR) scores for clinical assessment 5. Export results with full provenance tracking for reproducible research - + Key Capabilities: - Multiple alignment engines (MFA, Wav2TextGrid, WhisperX in development) - Extensible analyzer system for custom metadata extraction @@ -38,7 +38,7 @@ release_notes: | - Enhanced dataset analyzers with custom metadata extraction - Model management interface with version tracking - Startup routines for automated asset downloads - + Configuration Changes: - Researchers can now modify workflows by editing config/pipeline_definitions.yaml - No code changes required for common workflow adaptations @@ -49,10 +49,10 @@ contact_info: github_issues: "https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop/issues" email_support: "bfrey6@wisc.edu" documentation: "https://voxkit-web.vercel.app/help" - + # Research context research_context: | - VoxKit was developed through collaboration between WISCLab and the Brain Behavior - Analytics Lab to democratize access to state-of-the-art forced alignment tools. - The platform is designed around established speech pathology research methodologies + VoxKit was developed through collaboration between WISCLab and the Brain Behavior + Analytics Lab to democratize access to state-of-the-art forced alignment tools. + The platform is designed around established speech pathology research methodologies rather than generic audio processing workflows. diff --git a/installer/windows/VoxKit.iss b/installer/windows/VoxKit.iss index 0170b76..31c0c41 100644 --- a/installer/windows/VoxKit.iss +++ b/installer/windows/VoxKit.iss @@ -1,5 +1,14 @@ #define AppName "VoxKit" -#define AppVersion "0.4.1" + +; AppVersion is read from config/VERSION (single source of truth). +#define VersionFile "..\..\config\VERSION" +#define VersionHandle FileOpen(VersionFile) +#if VersionHandle + #define AppVersion Trim(FileRead(VersionHandle)) + #expr FileClose(VersionHandle) +#else + #error "Could not open config/VERSION" +#endif #define AppPublisher "Brain Behavior Analytics Lab" #define AppURL "https://github.com/BrainBehaviorAnalyticsLab/voxkit-desktop" #define AppExeName "VoxKit.exe" diff --git a/pyproject.toml b/pyproject.toml index b5c13ff..e9ecd48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,8 +3,8 @@ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" [project] name = "pypllr-gui" -version = "0.1.0" -description = "AI/ML Research -> Clinical Applications (Speech Pathology)" +dynamic = ["version"] +description = "PyQt6 workbench bridging audio ML engines and speech-pathology clinical workflows." readme = "README.md" requires-python = ">=3.11" license = {text = "MIT"} @@ -212,6 +212,9 @@ show_missing = true [tool.setuptools.packages.find] where = ["src"] +[tool.setuptools.dynamic] +version = {file = ["config/VERSION"]} + [tool.shredguard] [[tool.shredguard.patterns]] diff --git a/src/voxkit/__init__.py b/src/voxkit/__init__.py index 1ae30d9..94d69c0 100644 --- a/src/voxkit/__init__.py +++ b/src/voxkit/__init__.py @@ -10,12 +10,24 @@ - **config**: Application and pipeline configuration """ -__version__ = "0.4.0" -__author__ = "Beckett Frey - code@beckettfrey.com" +import sys +from pathlib import Path # Import subpackages for pdoc discoverability (not re-exported in __all__) from . import analyzers, config, engines, gui, storage + +def _read_version() -> str: + if getattr(sys, "_MEIPASS", None): + root = Path(getattr(sys, "_MEIPASS")) / "config" + else: + root = Path(__file__).resolve().parents[2] / "config" + return (root / "VERSION").read_text(encoding="utf-8").strip() + + +__version__ = _read_version() +__author__ = "Beckett Frey - code@beckettfrey.com" + __all__ = [ "__version__", "__author__", diff --git a/src/voxkit/config/app_config.py b/src/voxkit/config/app_config.py index 83d6328..3201aff 100644 --- a/src/voxkit/config/app_config.py +++ b/src/voxkit/config/app_config.py @@ -154,9 +154,14 @@ def from_yaml(cls, config_path: Path) -> "AppConfig": with open(config_path, "r", encoding="utf-8") as f: data = yaml.safe_load(f) + # Version is sourced from config/VERSION (single source of truth), + # not from per-profile YAML. + version_file = get_config_root() / "VERSION" + version = version_file.read_text(encoding="utf-8").strip() + return cls( app_name=data.get("app_name", "VoxKit"), - version=data.get("version", "0.0.0"), + version=version, description=data.get("description", ""), introduction=data.get("introduction", ""), help_url=data.get("help_url", "https://voxkit-web.vercel.app/help"), diff --git a/tests/config/test_app_config.py b/tests/config/test_app_config.py index e808e12..1bb288d 100644 --- a/tests/config/test_app_config.py +++ b/tests/config/test_app_config.py @@ -71,7 +71,6 @@ class TestAppConfigFromYaml: def test_from_yaml_success(self, tmp_path): yaml_content = """ app_name: MyApp -version: 1.2.3 description: My application description introduction: Welcome to MyApp help_url: http://myapp.com/help @@ -84,7 +83,6 @@ def test_from_yaml_success(self, tmp_path): config = AppConfig.from_yaml(config_file) assert config.app_name == "MyApp" - assert config.version == "1.2.3" assert config.description == "My application description" assert config.introduction == "Welcome to MyApp" assert config.help_url == "http://myapp.com/help" @@ -100,7 +98,6 @@ def test_from_yaml_with_defaults(self, tmp_path): config = AppConfig.from_yaml(config_file) assert config.app_name == "VoxKit" - assert config.version == "0.0.0" assert config.description == "" assert config.introduction == "" assert config.help_url == "https://voxkit-web.vercel.app/help" @@ -114,7 +111,6 @@ def test_from_yaml_with_defaults(self, tmp_path): def test_from_yaml_partial_config(self, tmp_path): yaml_content = """ app_name: PartialApp -version: 0.1.0 """ config_file = tmp_path / "app_info.yaml" config_file.write_text(yaml_content) @@ -122,11 +118,26 @@ def test_from_yaml_partial_config(self, tmp_path): config = AppConfig.from_yaml(config_file) assert config.app_name == "PartialApp" - assert config.version == "0.1.0" assert config.description == "" assert config.introduction == "" +class TestVersionFile: + """Verify the canonical version source (config/VERSION).""" + + def test_version_file_exists(self): + version_file = get_config_root() / "VERSION" + assert version_file.exists(), f"Expected canonical version file at {version_file}" + + def test_version_file_has_nonempty_version(self): + version = (get_config_root() / "VERSION").read_text(encoding="utf-8").strip() + assert version, "config/VERSION is empty" + # Loose sanity check: at least one digit and a dot (e.g. "0.4.1"). + assert any(ch.isdigit() for ch in version) and "." in version, ( + f"config/VERSION does not look like a version string: {version!r}" + ) + + class TestAppConfigLoadDefault: def test_load_default_returns_config(self): # This tests the actual config file in the project diff --git a/uv.lock b/uv.lock index 5456aa0..fab6d8a 100644 --- a/uv.lock +++ b/uv.lock @@ -2978,7 +2978,6 @@ wheels = [ [[package]] name = "pypllr-gui" -version = "0.1.0" source = { editable = "." } dependencies = [ { name = "accelerate" },