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.

@@ -0,0 +1,92 @@
1
+ """Hardware detection for service recommendations."""
2
+
3
+ import psutil
4
+
5
+ from .system import PlatformInfo
6
+
7
+
8
+ class HardwareInfo:
9
+ """Detect and provide hardware recommendations."""
10
+
11
+ def __init__(self, platform_info: PlatformInfo):
12
+ self.platform = platform_info
13
+ self.cpu_count = psutil.cpu_count(logical=False) or 1
14
+ self.total_ram_gb = psutil.virtual_memory().total / (1024 ** 3)
15
+
16
+ def is_apple_silicon(self) -> bool:
17
+ """Check if running on Apple Silicon."""
18
+ return self.platform.os_type == 'darwin' and self.platform.architecture == 'arm64'
19
+
20
+ def is_arm64(self) -> bool:
21
+ """Check if running on ARM64 architecture."""
22
+ return self.platform.architecture == 'arm64'
23
+
24
+ def get_ram_category(self) -> str:
25
+ """Categorize RAM amount."""
26
+ if self.total_ram_gb < 4:
27
+ return 'low'
28
+ elif self.total_ram_gb < 8:
29
+ return 'medium'
30
+ elif self.total_ram_gb < 16:
31
+ return 'good'
32
+ else:
33
+ return 'excellent'
34
+
35
+ def should_recommend_local_services(self) -> bool:
36
+ """Determine if local services should be recommended."""
37
+ # Apple Silicon is great for local services
38
+ if self.is_apple_silicon():
39
+ return True
40
+
41
+ # Other ARM64 with good RAM
42
+ if self.is_arm64() and self.total_ram_gb >= 8:
43
+ return True
44
+
45
+ # x86_64 with good specs
46
+ if self.total_ram_gb >= 8 and self.cpu_count >= 4:
47
+ return True
48
+
49
+ return False
50
+
51
+ def get_recommendation_message(self) -> str:
52
+ """Get a recommendation message for local services."""
53
+ if self.is_apple_silicon():
54
+ return (
55
+ f"Your Apple Silicon Mac with {self.total_ram_gb:.1f}GB RAM is great for local services.\n"
56
+ f"Whisper and Kokoro will run fast and privately on your hardware."
57
+ )
58
+ elif self.is_arm64():
59
+ if self.total_ram_gb >= 8:
60
+ return (
61
+ f"Your ARM64 system with {self.total_ram_gb:.1f}GB RAM can run local services well.\n"
62
+ f"Recommended for privacy and offline use."
63
+ )
64
+ else:
65
+ return (
66
+ f"Your ARM64 system has {self.total_ram_gb:.1f}GB RAM.\n"
67
+ f"Local services may work but cloud services might be more responsive."
68
+ )
69
+ else: # x86_64
70
+ if self.total_ram_gb >= 8 and self.cpu_count >= 4:
71
+ return (
72
+ f"Your system ({self.cpu_count} cores, {self.total_ram_gb:.1f}GB RAM) can run local services.\n"
73
+ f"Recommended for privacy and offline use."
74
+ )
75
+ elif self.total_ram_gb >= 4:
76
+ return (
77
+ f"Your system has {self.total_ram_gb:.1f}GB RAM.\n"
78
+ f"Local services will work but may be slower. Cloud services recommended for best performance."
79
+ )
80
+ else:
81
+ return (
82
+ f"Your system has {self.total_ram_gb:.1f}GB RAM.\n"
83
+ f"Cloud services strongly recommended - local services may struggle."
84
+ )
85
+
86
+ def get_download_estimate(self) -> str:
87
+ """Estimate download size for local services."""
88
+ # Rough estimates:
89
+ # Whisper: ~150MB (base model) to ~3GB (large model)
90
+ # Kokoro: ~500MB
91
+ # Total: ~2-4GB for full setup
92
+ return "~2-4GB total (Whisper models + Kokoro)"
@@ -0,0 +1,143 @@
1
+ """System package installation."""
2
+
3
+ import subprocess
4
+ from typing import List, Optional
5
+
6
+ from .checker import PackageInfo
7
+ from .system import PlatformInfo, get_package_manager
8
+
9
+
10
+ class PackageInstaller:
11
+ """Install system packages using platform-specific package managers."""
12
+
13
+ def __init__(self, platform_info: PlatformInfo, dry_run: bool = False):
14
+ self.platform = platform_info
15
+ self.dry_run = dry_run
16
+ self.package_manager = get_package_manager(platform_info.distribution)
17
+
18
+ def install_packages(self, packages: List[PackageInfo]) -> bool:
19
+ """
20
+ Install a list of packages.
21
+
22
+ Returns True if all installations succeeded, False otherwise.
23
+ """
24
+ if not packages:
25
+ return True
26
+
27
+ package_names = [pkg.name for pkg in packages]
28
+
29
+ if self.dry_run:
30
+ print(f"[DRY RUN] Would install: {', '.join(package_names)}")
31
+ return True
32
+
33
+ try:
34
+ if self.platform.distribution == 'darwin':
35
+ return self._install_homebrew(package_names)
36
+ elif self.platform.distribution == 'debian':
37
+ return self._install_apt(package_names)
38
+ elif self.platform.distribution == 'fedora':
39
+ return self._install_dnf(package_names)
40
+ else:
41
+ print(f"Error: Unsupported distribution: {self.platform.distribution}")
42
+ return False
43
+ except Exception as e:
44
+ print(f"Error installing packages: {e}")
45
+ return False
46
+
47
+ def _install_homebrew(self, packages: List[str]) -> bool:
48
+ """Install packages using Homebrew."""
49
+ try:
50
+ cmd = ['brew', 'install'] + packages
51
+ result = subprocess.run(
52
+ cmd,
53
+ check=True,
54
+ capture_output=False # Show output to user
55
+ )
56
+ return result.returncode == 0
57
+ except subprocess.CalledProcessError as e:
58
+ print(f"Homebrew installation failed: {e}")
59
+ return False
60
+ except FileNotFoundError:
61
+ print("Error: Homebrew not found. Please install Homebrew first.")
62
+ print("Visit: https://brew.sh")
63
+ return False
64
+
65
+ def _install_apt(self, packages: List[str]) -> bool:
66
+ """Install packages using apt."""
67
+ try:
68
+ # Update package lists first
69
+ print("Updating package lists...")
70
+ subprocess.run(
71
+ ['sudo', 'apt', 'update'],
72
+ check=True,
73
+ capture_output=False
74
+ )
75
+
76
+ # Install packages
77
+ cmd = ['sudo', 'apt', 'install', '-y'] + packages
78
+ result = subprocess.run(
79
+ cmd,
80
+ check=True,
81
+ capture_output=False
82
+ )
83
+ return result.returncode == 0
84
+ except subprocess.CalledProcessError as e:
85
+ print(f"apt installation failed: {e}")
86
+ return False
87
+ except FileNotFoundError:
88
+ print("Error: apt not found")
89
+ return False
90
+
91
+ def _install_dnf(self, packages: List[str]) -> bool:
92
+ """Install packages using dnf."""
93
+ try:
94
+ cmd = ['sudo', 'dnf', 'install', '-y'] + packages
95
+ result = subprocess.run(
96
+ cmd,
97
+ check=True,
98
+ capture_output=False
99
+ )
100
+ return result.returncode == 0
101
+ except subprocess.CalledProcessError as e:
102
+ print(f"dnf installation failed: {e}")
103
+ return False
104
+ except FileNotFoundError:
105
+ print("Error: dnf not found")
106
+ return False
107
+
108
+ def install_voicemode(self, version: Optional[str] = None) -> bool:
109
+ """
110
+ Install voice-mode using uv tool install.
111
+
112
+ Args:
113
+ version: Optional version to install (e.g., "5.1.3")
114
+
115
+ Returns:
116
+ True if installation succeeded, False otherwise.
117
+ """
118
+ if self.dry_run:
119
+ if version:
120
+ print(f"[DRY RUN] Would install: uv tool install voice-mode=={version}")
121
+ else:
122
+ print("[DRY RUN] Would install: uv tool install voice-mode")
123
+ return True
124
+
125
+ try:
126
+ if version:
127
+ cmd = ['uv', 'tool', 'install', f'voice-mode=={version}']
128
+ else:
129
+ cmd = ['uv', 'tool', 'install', 'voice-mode']
130
+
131
+ result = subprocess.run(
132
+ cmd,
133
+ check=True,
134
+ capture_output=False
135
+ )
136
+ return result.returncode == 0
137
+ except subprocess.CalledProcessError as e:
138
+ print(f"VoiceMode installation failed: {e}")
139
+ return False
140
+ except FileNotFoundError:
141
+ print("Error: uv not found. Please install uv first:")
142
+ print(" curl -LsSf https://astral.sh/uv/install.sh | sh")
143
+ return False
@@ -0,0 +1,90 @@
1
+ """Installation logging."""
2
+
3
+ import json
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any, Dict
7
+
8
+
9
+ class InstallLogger:
10
+ """Log installation progress and results."""
11
+
12
+ def __init__(self, log_path: Path = None):
13
+ if log_path is None:
14
+ voicemode_dir = Path.home() / '.voicemode'
15
+ voicemode_dir.mkdir(exist_ok=True)
16
+ log_path = voicemode_dir / 'install.log'
17
+
18
+ self.log_path = log_path
19
+ self.session_id = datetime.now().strftime('%Y%m%d_%H%M%S')
20
+ self.events = []
21
+
22
+ def log_event(self, event_type: str, message: str, details: Dict[str, Any] = None):
23
+ """Log an installation event."""
24
+ event = {
25
+ 'timestamp': datetime.now().isoformat(),
26
+ 'session_id': self.session_id,
27
+ 'type': event_type,
28
+ 'message': message,
29
+ }
30
+
31
+ if details:
32
+ event['details'] = details
33
+
34
+ self.events.append(event)
35
+
36
+ # Append to log file
37
+ with open(self.log_path, 'a') as f:
38
+ f.write(json.dumps(event) + '\n')
39
+
40
+ def log_start(self, system_info: Dict[str, Any]):
41
+ """Log installation start."""
42
+ self.log_event('start', 'Installation started', {'system': system_info})
43
+
44
+ def log_check(self, component: str, packages_found: int, packages_missing: int):
45
+ """Log dependency check results."""
46
+ self.log_event(
47
+ 'check',
48
+ f'Checked {component} dependencies',
49
+ {
50
+ 'component': component,
51
+ 'found': packages_found,
52
+ 'missing': packages_missing
53
+ }
54
+ )
55
+
56
+ def log_install(self, package_type: str, packages: list, success: bool):
57
+ """Log package installation."""
58
+ self.log_event(
59
+ 'install',
60
+ f'{"Successfully installed" if success else "Failed to install"} {package_type} packages',
61
+ {
62
+ 'package_type': package_type,
63
+ 'packages': packages,
64
+ 'success': success
65
+ }
66
+ )
67
+
68
+ def log_error(self, message: str, error: Exception = None):
69
+ """Log an error."""
70
+ details = {'message': message}
71
+ if error:
72
+ details['error'] = str(error)
73
+ details['error_type'] = type(error).__name__
74
+
75
+ self.log_event('error', message, details)
76
+
77
+ def log_complete(self, success: bool, voicemode_installed: bool):
78
+ """Log installation completion."""
79
+ self.log_event(
80
+ 'complete',
81
+ 'Installation completed' if success else 'Installation failed',
82
+ {
83
+ 'success': success,
84
+ 'voicemode_installed': voicemode_installed
85
+ }
86
+ )
87
+
88
+ def get_log_path(self) -> str:
89
+ """Get the path to the log file."""
90
+ return str(self.log_path)
@@ -0,0 +1,141 @@
1
+ """System detection and platform utilities."""
2
+
3
+ import platform
4
+ import shutil
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Optional
9
+
10
+
11
+ @dataclass
12
+ class PlatformInfo:
13
+ """Information about the current platform."""
14
+
15
+ os_type: str # darwin, linux, windows
16
+ os_name: str # macOS, Ubuntu, Fedora, etc.
17
+ distribution: str # debian, fedora, darwin, etc. (for yaml lookup)
18
+ architecture: str # arm64, x86_64
19
+ is_wsl: bool = False
20
+
21
+
22
+ def detect_platform() -> PlatformInfo:
23
+ """Detect the current platform and return platform information."""
24
+ os_type = platform.system().lower()
25
+ architecture = platform.machine().lower()
26
+
27
+ # Normalize architecture names
28
+ if architecture in ('amd64', 'x86_64', 'x64'):
29
+ architecture = 'x86_64'
30
+ elif architecture in ('aarch64', 'arm64'):
31
+ architecture = 'arm64'
32
+
33
+ # Detect WSL
34
+ is_wsl = False
35
+ if os_type == 'linux':
36
+ try:
37
+ with open('/proc/version', 'r') as f:
38
+ is_wsl = 'microsoft' in f.read().lower() or 'wsl' in f.read().lower()
39
+ except FileNotFoundError:
40
+ pass
41
+
42
+ if os_type == 'darwin':
43
+ return PlatformInfo(
44
+ os_type='darwin',
45
+ os_name='macOS',
46
+ distribution='darwin',
47
+ architecture=architecture,
48
+ is_wsl=False
49
+ )
50
+ elif os_type == 'linux':
51
+ # Detect Linux distribution
52
+ distro_info = _detect_linux_distro()
53
+ return PlatformInfo(
54
+ os_type='linux',
55
+ os_name=distro_info['name'],
56
+ distribution=distro_info['family'],
57
+ architecture=architecture,
58
+ is_wsl=is_wsl
59
+ )
60
+ else:
61
+ raise RuntimeError(f"Unsupported operating system: {os_type}")
62
+
63
+
64
+ def _detect_linux_distro() -> dict:
65
+ """Detect Linux distribution and family."""
66
+ # Try /etc/os-release first (most modern distros)
67
+ if Path('/etc/os-release').exists():
68
+ os_release = {}
69
+ with open('/etc/os-release') as f:
70
+ for line in f:
71
+ if '=' in line:
72
+ key, value = line.strip().split('=', 1)
73
+ os_release[key] = value.strip('"')
74
+
75
+ distro_id = os_release.get('ID', '').lower()
76
+ distro_id_like = os_release.get('ID_LIKE', '').lower()
77
+ distro_name = os_release.get('NAME', 'Linux')
78
+
79
+ # Determine family for yaml lookup
80
+ if distro_id in ('ubuntu', 'debian') or 'debian' in distro_id_like or 'ubuntu' in distro_id_like:
81
+ return {'name': distro_name, 'family': 'debian'}
82
+ elif distro_id in ('fedora', 'rhel', 'centos', 'rocky', 'alma') or 'fedora' in distro_id_like or 'rhel' in distro_id_like:
83
+ return {'name': distro_name, 'family': 'fedora'}
84
+ elif distro_id == 'arch' or 'arch' in distro_id_like:
85
+ return {'name': distro_name, 'family': 'arch'}
86
+ elif distro_id == 'alpine':
87
+ return {'name': distro_name, 'family': 'alpine'}
88
+ elif distro_id in ('opensuse', 'suse') or 'suse' in distro_id_like:
89
+ return {'name': distro_name, 'family': 'suse'}
90
+ else:
91
+ # Try to infer from ID_LIKE
92
+ if 'debian' in distro_id_like:
93
+ return {'name': distro_name, 'family': 'debian'}
94
+ elif 'fedora' in distro_id_like or 'rhel' in distro_id_like:
95
+ return {'name': distro_name, 'family': 'fedora'}
96
+
97
+ # Fallback: check for package managers
98
+ if Path('/usr/bin/apt').exists() or Path('/usr/bin/apt-get').exists():
99
+ return {'name': 'Debian-based Linux', 'family': 'debian'}
100
+ elif Path('/usr/bin/dnf').exists() or Path('/usr/bin/yum').exists():
101
+ return {'name': 'Fedora-based Linux', 'family': 'fedora'}
102
+
103
+ raise RuntimeError("Unable to detect Linux distribution")
104
+
105
+
106
+ def get_package_manager(distribution: str) -> str:
107
+ """Get the package manager command for the distribution."""
108
+ if distribution == 'darwin':
109
+ return 'brew'
110
+ elif distribution == 'debian':
111
+ return 'apt'
112
+ elif distribution == 'fedora':
113
+ return 'dnf'
114
+ elif distribution == 'arch':
115
+ return 'pacman'
116
+ elif distribution == 'alpine':
117
+ return 'apk'
118
+ elif distribution == 'suse':
119
+ return 'zypper'
120
+ else:
121
+ raise ValueError(f"Unknown distribution: {distribution}")
122
+
123
+
124
+ def check_command_exists(command: str) -> bool:
125
+ """Check if a command exists in PATH."""
126
+ return shutil.which(command) is not None
127
+
128
+
129
+ def get_system_info() -> dict:
130
+ """Get comprehensive system information for logging."""
131
+ platform_info = detect_platform()
132
+
133
+ return {
134
+ 'os_type': platform_info.os_type,
135
+ 'os_name': platform_info.os_name,
136
+ 'distribution': platform_info.distribution,
137
+ 'architecture': platform_info.architecture,
138
+ 'is_wsl': platform_info.is_wsl,
139
+ 'python_version': platform.python_version(),
140
+ 'platform': platform.platform(),
141
+ }