voice-mode-install 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of voice-mode-install might be problematic. Click here for more details.
- voice_mode_install-1.0.1.dist-info/METADATA +189 -0
- voice_mode_install-1.0.1.dist-info/RECORD +12 -0
- voice_mode_install-1.0.1.dist-info/WHEEL +4 -0
- voice_mode_install-1.0.1.dist-info/entry_points.txt +2 -0
- voicemode_install/__init__.py +3 -0
- voicemode_install/checker.py +191 -0
- voicemode_install/cli.py +297 -0
- voicemode_install/dependencies.yaml +359 -0
- voicemode_install/hardware.py +92 -0
- voicemode_install/installer.py +143 -0
- voicemode_install/logger.py +90 -0
- voicemode_install/system.py +141 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: voice-mode-install
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Installer for VoiceMode - handles system dependencies and installation
|
|
5
|
+
Project-URL: Homepage, https://github.com/mbailey/voicemode
|
|
6
|
+
Project-URL: Repository, https://github.com/mbailey/voicemode
|
|
7
|
+
Project-URL: Issues, https://github.com/mbailey/voicemode/issues
|
|
8
|
+
Author-email: mbailey <mbailey@example.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: installer,mcp,setup,voice,voicemode
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Installation/Setup
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: click>=8.0.0
|
|
23
|
+
Requires-Dist: psutil>=5.9.0
|
|
24
|
+
Requires-Dist: pyyaml>=6.0.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# voicemode-install
|
|
28
|
+
|
|
29
|
+
A standalone installer package for VoiceMode that handles system dependency detection and installation.
|
|
30
|
+
|
|
31
|
+
## Overview
|
|
32
|
+
|
|
33
|
+
`voicemode-install` simplifies the VoiceMode installation process by:
|
|
34
|
+
|
|
35
|
+
1. **Detecting your platform** - Identifies your OS, distribution, and architecture
|
|
36
|
+
2. **Checking dependencies** - Scans for required system packages
|
|
37
|
+
3. **Installing packages** - Uses your system's package manager (apt, dnf, brew)
|
|
38
|
+
4. **Installing VoiceMode** - Runs `uv tool install voice-mode`
|
|
39
|
+
5. **Hardware recommendations** - Suggests optimal configuration for your system
|
|
40
|
+
6. **Logging everything** - Saves installation logs for troubleshooting
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Install and run
|
|
46
|
+
uvx voicemode-install
|
|
47
|
+
|
|
48
|
+
# Dry run (see what would be installed)
|
|
49
|
+
uvx voicemode-install --dry-run
|
|
50
|
+
|
|
51
|
+
# Install specific version
|
|
52
|
+
uvx voicemode-install --voice-mode-version=5.1.3
|
|
53
|
+
|
|
54
|
+
# Skip service installation
|
|
55
|
+
uvx voicemode-install --skip-services
|
|
56
|
+
|
|
57
|
+
# Non-interactive mode
|
|
58
|
+
uvx voicemode-install --non-interactive
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Prerequisites
|
|
62
|
+
|
|
63
|
+
- **uv** - Required to run the installer (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
|
|
64
|
+
- **Python 3.10+** - Usually pre-installed on modern systems
|
|
65
|
+
- **sudo access** - Needed to install system packages (Linux)
|
|
66
|
+
|
|
67
|
+
## Supported Platforms
|
|
68
|
+
|
|
69
|
+
- **macOS** - Intel and Apple Silicon (via Homebrew)
|
|
70
|
+
- **Ubuntu/Debian** - Using apt package manager
|
|
71
|
+
- **Fedora/RHEL** - Using dnf package manager
|
|
72
|
+
|
|
73
|
+
## Features
|
|
74
|
+
|
|
75
|
+
### Phase 1 (Included)
|
|
76
|
+
|
|
77
|
+
✅ **Dry-run Mode** - Preview what will be installed
|
|
78
|
+
✅ **Installation Logging** - Detailed logs saved to `~/.voicemode/install.log`
|
|
79
|
+
✅ **Shell Completion** - Auto-configures tab completion for bash/zsh
|
|
80
|
+
✅ **Health Check** - Verifies installation after completion
|
|
81
|
+
✅ **Version Pinning** - Install specific VoiceMode versions
|
|
82
|
+
✅ **Hardware Detection** - Recommends optimal setup for your system
|
|
83
|
+
|
|
84
|
+
### Phase 2 (Future)
|
|
85
|
+
|
|
86
|
+
⏱️ Config Validation - Check for conflicting settings
|
|
87
|
+
⏱️ Uninstall Support - Clean removal of VoiceMode
|
|
88
|
+
|
|
89
|
+
## How It Works
|
|
90
|
+
|
|
91
|
+
1. **Platform Detection** - Identifies OS, distribution, and architecture
|
|
92
|
+
2. **Dependency Checking** - Compares installed packages against `dependencies.yaml`
|
|
93
|
+
3. **Package Installation** - Uses platform-specific package managers:
|
|
94
|
+
- macOS: `brew install`
|
|
95
|
+
- Ubuntu/Debian: `sudo apt install`
|
|
96
|
+
- Fedora: `sudo dnf install`
|
|
97
|
+
4. **VoiceMode Installation** - Runs `uv tool install voice-mode[==version]`
|
|
98
|
+
5. **Post-Install** - Configures shell completion and verifies installation
|
|
99
|
+
|
|
100
|
+
## Installation Logs
|
|
101
|
+
|
|
102
|
+
Logs are saved to `~/.voicemode/install.log` in JSONL format:
|
|
103
|
+
|
|
104
|
+
```json
|
|
105
|
+
{"timestamp": "2025-10-12T10:30:00", "type": "start", "message": "Installation started"}
|
|
106
|
+
{"timestamp": "2025-10-12T10:30:15", "type": "check", "message": "Checked core dependencies"}
|
|
107
|
+
{"timestamp": "2025-10-12T10:30:45", "type": "install", "message": "Successfully installed system packages"}
|
|
108
|
+
{"timestamp": "2025-10-12T10:31:30", "type": "complete", "message": "Installation completed"}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Troubleshooting
|
|
112
|
+
|
|
113
|
+
### VoiceMode command not found after installation
|
|
114
|
+
|
|
115
|
+
Restart your shell or run:
|
|
116
|
+
```bash
|
|
117
|
+
source ~/.bashrc # or ~/.zshrc for zsh
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Permission denied during package installation
|
|
121
|
+
|
|
122
|
+
The installer needs sudo access to install system packages. Run:
|
|
123
|
+
```bash
|
|
124
|
+
sudo -v # Refresh sudo credentials
|
|
125
|
+
uvx voicemode-install
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Network errors during installation
|
|
129
|
+
|
|
130
|
+
- Check your internet connection
|
|
131
|
+
- Try again with: `uvx voicemode-install`
|
|
132
|
+
- Use `uvx --refresh voicemode-install` to get the latest installer
|
|
133
|
+
|
|
134
|
+
### Installation hangs or fails
|
|
135
|
+
|
|
136
|
+
1. Check the log file: `~/.voicemode/install.log`
|
|
137
|
+
2. Try a dry run: `uvx voicemode-install --dry-run`
|
|
138
|
+
3. Report issues with log file attached
|
|
139
|
+
|
|
140
|
+
## Development
|
|
141
|
+
|
|
142
|
+
### Building from Source
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
cd installer/
|
|
146
|
+
uv build
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Testing Locally
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
cd installer/
|
|
153
|
+
uv pip install -e .
|
|
154
|
+
voicemode-install --dry-run
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Project Structure
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
installer/
|
|
161
|
+
├── pyproject.toml # Package definition
|
|
162
|
+
├── voicemode_install/
|
|
163
|
+
│ ├── __init__.py # Version and exports
|
|
164
|
+
│ ├── cli.py # Main CLI entry point
|
|
165
|
+
│ ├── system.py # Platform detection
|
|
166
|
+
│ ├── checker.py # Dependency checking
|
|
167
|
+
│ ├── installer.py # Package installation
|
|
168
|
+
│ ├── hardware.py # Hardware detection
|
|
169
|
+
│ ├── logger.py # Installation logging
|
|
170
|
+
│ └── dependencies.yaml # System dependencies
|
|
171
|
+
└── README.md
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Design Decisions
|
|
175
|
+
|
|
176
|
+
See `DECISIONS.md` in the task directory for detailed rationale behind:
|
|
177
|
+
- Version management strategy
|
|
178
|
+
- Dependency synchronization approach
|
|
179
|
+
- Error handling philosophy
|
|
180
|
+
- Platform coverage priorities
|
|
181
|
+
- Service installation scope
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
This installer is part of the [VoiceMode](https://github.com/mbailey/voicemode) project.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT License - Same as VoiceMode
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
voicemode_install/__init__.py,sha256=xKbCS78wqA7H42wWj8a3VgpYQ700boLgSgyRW_7P_WE,98
|
|
2
|
+
voicemode_install/checker.py,sha256=yXfLYkPxCKK64hQKQuuaIkZly_0GksVmNHpzV3gKdRI,6766
|
|
3
|
+
voicemode_install/cli.py,sha256=BMdiDDbutinjUXY9k6gkt11QGYu5YThaT5DMCX1DyIg,12166
|
|
4
|
+
voicemode_install/dependencies.yaml,sha256=xaOt6UKY-XwFI8HFanUUd2zrxMWskkgBevYXZSEpvOM,12550
|
|
5
|
+
voicemode_install/hardware.py,sha256=j4bDv1v8bqyhiysWsTD4F-qEJRG-a8Z1o-qPfxofQt4,3529
|
|
6
|
+
voicemode_install/installer.py,sha256=vS2G0t6a1jZcxls_go7oDMqnNYLHZF2H_gZa1dRusx0,4920
|
|
7
|
+
voicemode_install/logger.py,sha256=UpNegF2D-y-7RthzhzSFMiHPRaFV09F2AUxgjm5b2Pw,2859
|
|
8
|
+
voicemode_install/system.py,sha256=TBWdP9pNTH9BtaZwe4NyzvEDoQuBCj6gqHtJyAyFiqo,4971
|
|
9
|
+
voice_mode_install-1.0.1.dist-info/METADATA,sha256=xyFE0w1Ee-W9jwpP1L8EDjnpq5xTDaHYq9O2LfTtYkg,5928
|
|
10
|
+
voice_mode_install-1.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
11
|
+
voice_mode_install-1.0.1.dist-info/entry_points.txt,sha256=Xq6JCA8TzWWoQjvito_nT8SM2RcNEBu4OBtp9pz8JR8,65
|
|
12
|
+
voice_mode_install-1.0.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Dependency checking logic."""
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from .system import PlatformInfo, check_command_exists
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class PackageInfo:
|
|
15
|
+
"""Information about a system package."""
|
|
16
|
+
|
|
17
|
+
name: str
|
|
18
|
+
description: str
|
|
19
|
+
required: bool
|
|
20
|
+
installed: bool
|
|
21
|
+
check_command: Optional[str] = None
|
|
22
|
+
min_version: Optional[str] = None
|
|
23
|
+
note: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DependencyChecker:
|
|
27
|
+
"""Check system dependencies against dependencies.yaml."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, platform_info: PlatformInfo):
|
|
30
|
+
self.platform = platform_info
|
|
31
|
+
self.dependencies = self._load_dependencies()
|
|
32
|
+
|
|
33
|
+
def _load_dependencies(self) -> dict:
|
|
34
|
+
"""Load dependencies from bundled YAML file."""
|
|
35
|
+
deps_file = Path(__file__).parent / 'dependencies.yaml'
|
|
36
|
+
with open(deps_file) as f:
|
|
37
|
+
return yaml.safe_load(f)
|
|
38
|
+
|
|
39
|
+
def check_core_dependencies(self) -> List[PackageInfo]:
|
|
40
|
+
"""Check core VoiceMode dependencies."""
|
|
41
|
+
return self._check_component_dependencies('core')
|
|
42
|
+
|
|
43
|
+
def check_whisper_dependencies(self) -> List[PackageInfo]:
|
|
44
|
+
"""Check Whisper STT dependencies."""
|
|
45
|
+
return self._check_component_dependencies('whisper')
|
|
46
|
+
|
|
47
|
+
def check_kokoro_dependencies(self) -> List[PackageInfo]:
|
|
48
|
+
"""Check Kokoro TTS dependencies."""
|
|
49
|
+
return self._check_component_dependencies('kokoro')
|
|
50
|
+
|
|
51
|
+
def _check_component_dependencies(self, component: str) -> List[PackageInfo]:
|
|
52
|
+
"""Check dependencies for a specific component."""
|
|
53
|
+
packages = []
|
|
54
|
+
|
|
55
|
+
# Get the component section
|
|
56
|
+
voicemode_deps = self.dependencies.get('voicemode', {})
|
|
57
|
+
component_deps = voicemode_deps.get(component, {})
|
|
58
|
+
|
|
59
|
+
if not component_deps:
|
|
60
|
+
return packages
|
|
61
|
+
|
|
62
|
+
# Check common packages first (all platforms)
|
|
63
|
+
if 'common' in component_deps:
|
|
64
|
+
packages.extend(self._check_packages(component_deps['common'].get('packages', [])))
|
|
65
|
+
|
|
66
|
+
# Check platform-specific packages
|
|
67
|
+
if self.platform.distribution in component_deps:
|
|
68
|
+
platform_packages = component_deps[self.platform.distribution].get('packages', [])
|
|
69
|
+
packages.extend(self._check_packages(platform_packages))
|
|
70
|
+
|
|
71
|
+
return packages
|
|
72
|
+
|
|
73
|
+
def _check_packages(self, package_list: List[dict]) -> List[PackageInfo]:
|
|
74
|
+
"""Check a list of packages and return their status."""
|
|
75
|
+
results = []
|
|
76
|
+
|
|
77
|
+
for pkg_dict in package_list:
|
|
78
|
+
name = pkg_dict.get('name')
|
|
79
|
+
if not name:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
description = pkg_dict.get('description', '')
|
|
83
|
+
required = pkg_dict.get('required', True)
|
|
84
|
+
check_command = pkg_dict.get('check_command')
|
|
85
|
+
min_version = pkg_dict.get('min_version')
|
|
86
|
+
note = pkg_dict.get('note')
|
|
87
|
+
|
|
88
|
+
# Handle WSL-specific requirements
|
|
89
|
+
if required == 'wsl':
|
|
90
|
+
required = self.platform.is_wsl
|
|
91
|
+
|
|
92
|
+
# Check if package is installed
|
|
93
|
+
installed = self._is_package_installed(name, check_command)
|
|
94
|
+
|
|
95
|
+
results.append(PackageInfo(
|
|
96
|
+
name=name,
|
|
97
|
+
description=description,
|
|
98
|
+
required=bool(required),
|
|
99
|
+
installed=installed,
|
|
100
|
+
check_command=check_command,
|
|
101
|
+
min_version=min_version,
|
|
102
|
+
note=note
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
return results
|
|
106
|
+
|
|
107
|
+
def _is_package_installed(self, package_name: str, check_command: Optional[str] = None) -> bool:
|
|
108
|
+
"""Check if a package is installed."""
|
|
109
|
+
# If a check command is provided, use it
|
|
110
|
+
if check_command:
|
|
111
|
+
return self._run_check_command(check_command)
|
|
112
|
+
|
|
113
|
+
# Otherwise, use package manager-specific checks
|
|
114
|
+
if self.platform.distribution == 'darwin':
|
|
115
|
+
return self._check_homebrew_package(package_name)
|
|
116
|
+
elif self.platform.distribution == 'debian':
|
|
117
|
+
return self._check_apt_package(package_name)
|
|
118
|
+
elif self.platform.distribution == 'fedora':
|
|
119
|
+
return self._check_dnf_package(package_name)
|
|
120
|
+
else:
|
|
121
|
+
# Fallback: check if command exists
|
|
122
|
+
return check_command_exists(package_name)
|
|
123
|
+
|
|
124
|
+
def _run_check_command(self, command: str) -> bool:
|
|
125
|
+
"""Run a check command and return whether it succeeded."""
|
|
126
|
+
try:
|
|
127
|
+
subprocess.run(
|
|
128
|
+
command,
|
|
129
|
+
shell=True,
|
|
130
|
+
capture_output=True,
|
|
131
|
+
check=True,
|
|
132
|
+
timeout=5
|
|
133
|
+
)
|
|
134
|
+
return True
|
|
135
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def _check_homebrew_package(self, package_name: str) -> bool:
|
|
139
|
+
"""Check if a Homebrew package is installed."""
|
|
140
|
+
try:
|
|
141
|
+
subprocess.run(
|
|
142
|
+
['brew', 'list', package_name],
|
|
143
|
+
capture_output=True,
|
|
144
|
+
check=True
|
|
145
|
+
)
|
|
146
|
+
return True
|
|
147
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def _check_apt_package(self, package_name: str) -> bool:
|
|
151
|
+
"""Check if an apt package is installed."""
|
|
152
|
+
try:
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
['dpkg', '-l', package_name],
|
|
155
|
+
capture_output=True,
|
|
156
|
+
text=True
|
|
157
|
+
)
|
|
158
|
+
# dpkg -l returns 0 even if package not found, check output
|
|
159
|
+
return result.returncode == 0 and package_name in result.stdout
|
|
160
|
+
except FileNotFoundError:
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
def _check_dnf_package(self, package_name: str) -> bool:
|
|
164
|
+
"""Check if a dnf/yum package is installed."""
|
|
165
|
+
try:
|
|
166
|
+
subprocess.run(
|
|
167
|
+
['rpm', '-q', package_name],
|
|
168
|
+
capture_output=True,
|
|
169
|
+
check=True
|
|
170
|
+
)
|
|
171
|
+
return True
|
|
172
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def get_missing_packages(self, packages: List[PackageInfo]) -> List[PackageInfo]:
|
|
176
|
+
"""Filter to only required packages that are not installed."""
|
|
177
|
+
return [pkg for pkg in packages if pkg.required and not pkg.installed]
|
|
178
|
+
|
|
179
|
+
def get_summary(self, packages: List[PackageInfo]) -> dict:
|
|
180
|
+
"""Get a summary of package status."""
|
|
181
|
+
total = len(packages)
|
|
182
|
+
required = sum(1 for pkg in packages if pkg.required)
|
|
183
|
+
installed = sum(1 for pkg in packages if pkg.installed)
|
|
184
|
+
missing_required = sum(1 for pkg in packages if pkg.required and not pkg.installed)
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
'total': total,
|
|
188
|
+
'required': required,
|
|
189
|
+
'installed': installed,
|
|
190
|
+
'missing_required': missing_required
|
|
191
|
+
}
|