voice-mode 2.27.0__py3-none-any.whl → 2.28.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.
- voice_mode/__version__.py +1 -1
- voice_mode/cli.py +152 -37
- voice_mode/cli_commands/exchanges.py +6 -0
- voice_mode/frontend/.next/BUILD_ID +1 -1
- voice_mode/frontend/.next/app-build-manifest.json +5 -5
- voice_mode/frontend/.next/build-manifest.json +3 -3
- voice_mode/frontend/.next/next-minimal-server.js.nft.json +1 -1
- voice_mode/frontend/.next/next-server.js.nft.json +1 -1
- voice_mode/frontend/.next/prerender-manifest.json +1 -1
- voice_mode/frontend/.next/required-server-files.json +1 -1
- voice_mode/frontend/.next/server/app/_not-found/page.js +1 -1
- voice_mode/frontend/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/server/app/_not-found.html +1 -1
- voice_mode/frontend/.next/server/app/_not-found.rsc +1 -1
- voice_mode/frontend/.next/server/app/api/connection-details/route.js +2 -2
- voice_mode/frontend/.next/server/app/favicon.ico/route.js +2 -2
- voice_mode/frontend/.next/server/app/index.html +1 -1
- voice_mode/frontend/.next/server/app/index.rsc +2 -2
- voice_mode/frontend/.next/server/app/page.js +2 -2
- voice_mode/frontend/.next/server/app/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/server/chunks/994.js +1 -1
- voice_mode/frontend/.next/server/middleware-build-manifest.js +1 -1
- voice_mode/frontend/.next/server/next-font-manifest.js +1 -1
- voice_mode/frontend/.next/server/next-font-manifest.json +1 -1
- voice_mode/frontend/.next/server/pages/404.html +1 -1
- voice_mode/frontend/.next/server/pages/500.html +1 -1
- voice_mode/frontend/.next/server/server-reference-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/BUILD_ID +1 -1
- voice_mode/frontend/.next/standalone/.next/app-build-manifest.json +5 -5
- voice_mode/frontend/.next/standalone/.next/build-manifest.json +3 -3
- voice_mode/frontend/.next/standalone/.next/prerender-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/required-server-files.json +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/api/connection-details/route.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/favicon.ico/route.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/index.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/app/index.rsc +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/page.js +2 -2
- voice_mode/frontend/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/chunks/994.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/middleware-build-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.js +1 -1
- voice_mode/frontend/.next/standalone/.next/server/next-font-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/.next/server/pages/404.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/pages/500.html +1 -1
- voice_mode/frontend/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- voice_mode/frontend/.next/standalone/server.js +1 -1
- voice_mode/frontend/.next/static/chunks/app/{layout-08be62ed6e344292.js → layout-2a1721553cbe58e4.js} +1 -1
- voice_mode/frontend/.next/static/chunks/app/page-fe35d9da20297c85.js +1 -0
- voice_mode/frontend/.next/static/chunks/{main-app-413f77c1f2c53e3f.js → main-app-c17195caa4e269d6.js} +1 -1
- voice_mode/frontend/.next/trace +43 -43
- voice_mode/frontend/.next/types/app/api/connection-details/route.ts +1 -1
- voice_mode/frontend/.next/types/app/layout.ts +1 -1
- voice_mode/frontend/.next/types/app/page.ts +1 -1
- voice_mode/frontend/package-lock.json +6 -6
- voice_mode/tools/converse.py +44 -24
- voice_mode/tools/service.py +30 -3
- voice_mode/tools/services/kokoro/install.py +1 -1
- voice_mode/tools/services/whisper/__init__.py +15 -5
- voice_mode/tools/services/whisper/install.py +41 -9
- voice_mode/tools/services/whisper/list_models.py +14 -14
- voice_mode/tools/services/whisper/model_active.py +54 -0
- voice_mode/tools/services/whisper/model_benchmark.py +159 -0
- voice_mode/tools/services/whisper/{download_model.py → model_install.py} +72 -11
- voice_mode/tools/services/whisper/model_remove.py +36 -0
- voice_mode/tools/services/whisper/models.py +225 -26
- voice_mode/utils/services/whisper_helpers.py +206 -19
- voice_mode/utils/services/whisper_version.py +138 -0
- {voice_mode-2.27.0.dist-info → voice_mode-2.28.1.dist-info}/METADATA +5 -1
- {voice_mode-2.27.0.dist-info → voice_mode-2.28.1.dist-info}/RECORD +77 -74
- voice_mode/frontend/.next/static/chunks/app/page-80fc72669f25298f.js +0 -1
- voice_mode/tools/services/whisper/list_models_tool.py +0 -65
- /voice_mode/frontend/.next/static/{wQ5pxzPmwjlzdUfJwSjMg → LhJalgfazyY_l3L_v0_Kw}/_buildManifest.js +0 -0
- /voice_mode/frontend/.next/static/{wQ5pxzPmwjlzdUfJwSjMg → LhJalgfazyY_l3L_v0_Kw}/_ssgManifest.js +0 -0
- {voice_mode-2.27.0.dist-info → voice_mode-2.28.1.dist-info}/WHEEL +0 -0
- {voice_mode-2.27.0.dist-info → voice_mode-2.28.1.dist-info}/entry_points.txt +0 -0
@@ -1,9 +1,11 @@
|
|
1
1
|
"""Helper functions for whisper service management."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
import re
|
4
5
|
import subprocess
|
5
6
|
import platform
|
6
7
|
import logging
|
8
|
+
import shutil
|
7
9
|
from pathlib import Path
|
8
10
|
from typing import Optional, List, Dict, Union
|
9
11
|
|
@@ -108,7 +110,6 @@ async def download_whisper_model(
|
|
108
110
|
break
|
109
111
|
|
110
112
|
if original_script:
|
111
|
-
import shutil
|
112
113
|
shutil.copy2(original_script, download_script)
|
113
114
|
os.chmod(download_script, 0o755)
|
114
115
|
else:
|
@@ -116,7 +117,6 @@ async def download_whisper_model(
|
|
116
117
|
# (happens during install when models_dir is install_dir/models)
|
117
118
|
parent_script = models_dir.parent / "models" / "download-ggml-model.sh"
|
118
119
|
if parent_script.exists() and parent_script != download_script:
|
119
|
-
import shutil
|
120
120
|
shutil.copy2(parent_script, download_script)
|
121
121
|
os.chmod(download_script, 0o755)
|
122
122
|
else:
|
@@ -146,16 +146,52 @@ async def download_whisper_model(
|
|
146
146
|
|
147
147
|
# Check for Core ML support on Apple Silicon
|
148
148
|
if platform.system() == "Darwin" and platform.machine() == "arm64":
|
149
|
+
# Check if Core ML dependencies are needed
|
150
|
+
requirements_file = Path(models_dir) / "requirements-coreml.txt"
|
151
|
+
if requirements_file.exists() and shutil.which("uv"):
|
152
|
+
# Try to check if torch is available
|
153
|
+
try:
|
154
|
+
subprocess.run(
|
155
|
+
["uv", "run", "python", "-c", "import torch"],
|
156
|
+
capture_output=True,
|
157
|
+
check=True,
|
158
|
+
timeout=5
|
159
|
+
)
|
160
|
+
torch_available = True
|
161
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
162
|
+
torch_available = False
|
163
|
+
|
164
|
+
if not torch_available:
|
165
|
+
logger.info("Installing Core ML dependencies for optimal performance...")
|
166
|
+
try:
|
167
|
+
subprocess.run(
|
168
|
+
["uv", "pip", "install", "-r", str(requirements_file)],
|
169
|
+
capture_output=True,
|
170
|
+
check=True,
|
171
|
+
timeout=120
|
172
|
+
)
|
173
|
+
logger.info("Core ML dependencies installed successfully")
|
174
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
175
|
+
logger.info("Could not install Core ML dependencies automatically. Whisper will still work with Metal acceleration.")
|
176
|
+
|
149
177
|
core_ml_result = await convert_to_coreml(model, models_dir)
|
150
178
|
if core_ml_result["success"]:
|
151
179
|
logger.info(f"Core ML conversion completed for {model}")
|
152
180
|
else:
|
153
|
-
|
181
|
+
# Log appropriate level based on error category
|
182
|
+
error_category = core_ml_result.get('error_category', 'unknown')
|
183
|
+
if error_category in ['missing_pytorch', 'missing_coremltools', 'missing_whisper', 'missing_ane_transformers', 'missing_module']:
|
184
|
+
logger.info(f"Core ML conversion skipped - {core_ml_result.get('error', 'Missing dependencies')}. Whisper will use Metal acceleration.")
|
185
|
+
else:
|
186
|
+
logger.warning(f"Core ML conversion failed ({error_category}): {core_ml_result.get('error', 'Unknown error')}")
|
154
187
|
|
188
|
+
# Always include Core ML status in response
|
155
189
|
return {
|
156
190
|
"success": True,
|
157
191
|
"path": str(model_path),
|
158
|
-
"message": f"Model {model} downloaded successfully"
|
192
|
+
"message": f"Model {model} downloaded successfully",
|
193
|
+
"core_ml_status": core_ml_result,
|
194
|
+
"acceleration": "coreml" if core_ml_result.get("success") else "metal"
|
159
195
|
}
|
160
196
|
|
161
197
|
except subprocess.CalledProcessError as e:
|
@@ -200,26 +236,82 @@ async def convert_to_coreml(
|
|
200
236
|
}
|
201
237
|
|
202
238
|
# Find the Core ML conversion script
|
203
|
-
|
204
|
-
|
239
|
+
# Try new location first, then fall back to old location
|
240
|
+
whisper_dir = Path.home() / ".voicemode" / "services" / "whisper"
|
241
|
+
if not whisper_dir.exists():
|
242
|
+
whisper_dir = Path.home() / ".voicemode" / "whisper.cpp"
|
243
|
+
|
244
|
+
# Use the uv wrapper script if it exists, otherwise fallback to original
|
245
|
+
convert_script = whisper_dir / "models" / "generate-coreml-model-uv.sh"
|
246
|
+
if not convert_script.exists():
|
247
|
+
convert_script = whisper_dir / "models" / "generate-coreml-model.sh"
|
205
248
|
|
206
249
|
if not convert_script.exists():
|
207
250
|
return {
|
208
251
|
"success": False,
|
209
|
-
"error": "Core ML conversion script not found"
|
252
|
+
"error": f"Core ML conversion script not found at {convert_script}"
|
210
253
|
}
|
211
254
|
|
212
255
|
logger.info(f"Converting {model} to Core ML format...")
|
213
256
|
|
214
257
|
try:
|
215
|
-
#
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
258
|
+
# Check if we should use uv for Python dependencies
|
259
|
+
# Try to find the voicemode project root for uv
|
260
|
+
voicemode_root = None
|
261
|
+
current = Path(__file__).parent
|
262
|
+
while current != current.parent:
|
263
|
+
if (current / "pyproject.toml").exists():
|
264
|
+
with open(current / "pyproject.toml") as f:
|
265
|
+
content = f.read()
|
266
|
+
if 'name = "voice-mode"' in content or 'name = "voicemode"' in content:
|
267
|
+
voicemode_root = current
|
268
|
+
break
|
269
|
+
current = current.parent
|
270
|
+
|
271
|
+
# If we found voicemode root and uv is available, use it
|
272
|
+
if voicemode_root and shutil.which("uv"):
|
273
|
+
# Run the Python script directly with uv instead of using the bash wrapper
|
274
|
+
logger.info("Using uv for Core ML conversion with Python dependencies")
|
275
|
+
# Run from the whisper models directory
|
276
|
+
script_path = whisper_dir / "models" / "convert-whisper-to-coreml.py"
|
277
|
+
result = subprocess.run(
|
278
|
+
["uv", "run", "--project", str(voicemode_root), "python",
|
279
|
+
str(script_path),
|
280
|
+
"--model", model, "--encoder-only", "True", "--optimize-ane", "True"],
|
281
|
+
cwd=str(whisper_dir / "models"),
|
282
|
+
capture_output=True,
|
283
|
+
text=True,
|
284
|
+
check=True
|
285
|
+
)
|
286
|
+
|
287
|
+
# Now compile the mlpackage to mlmodelc using coremlc
|
288
|
+
mlpackage_path = models_dir / f"coreml-encoder-{model}.mlpackage"
|
289
|
+
if mlpackage_path.exists():
|
290
|
+
logger.info(f"Compiling Core ML model with coremlc...")
|
291
|
+
compile_result = subprocess.run(
|
292
|
+
["xcrun", "coremlc", "compile", str(mlpackage_path), str(models_dir)],
|
293
|
+
capture_output=True,
|
294
|
+
text=True,
|
295
|
+
check=True
|
296
|
+
)
|
297
|
+
|
298
|
+
# Move the compiled model to the correct name
|
299
|
+
compiled_path = models_dir / f"coreml-encoder-{model}.mlmodelc"
|
300
|
+
if compiled_path.exists():
|
301
|
+
shutil.rmtree(coreml_path, ignore_errors=True)
|
302
|
+
shutil.move(str(compiled_path), str(coreml_path))
|
303
|
+
else:
|
304
|
+
# Fallback to original bash script
|
305
|
+
logger.info("Using standard Python for Core ML conversion")
|
306
|
+
# Run from the whisper models directory where the script is located
|
307
|
+
script_dir = convert_script.parent
|
308
|
+
result = subprocess.run(
|
309
|
+
["bash", str(convert_script), model],
|
310
|
+
cwd=str(script_dir),
|
311
|
+
capture_output=True,
|
312
|
+
text=True,
|
313
|
+
check=True
|
314
|
+
)
|
223
315
|
|
224
316
|
if coreml_path.exists():
|
225
317
|
return {
|
@@ -234,16 +326,111 @@ async def convert_to_coreml(
|
|
234
326
|
}
|
235
327
|
|
236
328
|
except subprocess.CalledProcessError as e:
|
237
|
-
|
329
|
+
error_text = e.stderr if e.stderr else ""
|
330
|
+
stdout_text = e.stdout if e.stdout else ""
|
331
|
+
# Combine both for error detection since Python errors can appear in either
|
332
|
+
combined_output = error_text + stdout_text
|
333
|
+
|
334
|
+
# Enhanced error detection with specific categories
|
335
|
+
error_details = {
|
336
|
+
"success": False,
|
337
|
+
"error_type": "subprocess_error",
|
338
|
+
"return_code": e.returncode,
|
339
|
+
"command": " ".join(e.cmd) if hasattr(e, 'cmd') else "conversion script",
|
340
|
+
}
|
341
|
+
|
342
|
+
# Detect specific missing dependencies
|
343
|
+
if "ModuleNotFoundError" in combined_output:
|
344
|
+
if "torch" in combined_output:
|
345
|
+
error_details.update({
|
346
|
+
"error_category": "missing_pytorch",
|
347
|
+
"error": "PyTorch not installed - required for Core ML conversion",
|
348
|
+
"install_command": "uv pip install torch",
|
349
|
+
"manual_install": "pip install torch",
|
350
|
+
"package_size": "~2.5GB"
|
351
|
+
})
|
352
|
+
elif "coremltools" in combined_output:
|
353
|
+
error_details.update({
|
354
|
+
"error_category": "missing_coremltools",
|
355
|
+
"error": "CoreMLTools not installed",
|
356
|
+
"install_command": "uv pip install coremltools",
|
357
|
+
"manual_install": "pip install coremltools",
|
358
|
+
"package_size": "~50MB"
|
359
|
+
})
|
360
|
+
elif "whisper" in combined_output:
|
361
|
+
error_details.update({
|
362
|
+
"error_category": "missing_whisper",
|
363
|
+
"error": "OpenAI Whisper package not installed",
|
364
|
+
"install_command": "uv pip install openai-whisper",
|
365
|
+
"manual_install": "pip install openai-whisper",
|
366
|
+
"package_size": "~100MB"
|
367
|
+
})
|
368
|
+
elif "ane_transformers" in combined_output:
|
369
|
+
error_details.update({
|
370
|
+
"error_category": "missing_ane_transformers",
|
371
|
+
"error": "ANE Transformers not installed for Apple Neural Engine optimization",
|
372
|
+
"install_command": "uv pip install ane_transformers",
|
373
|
+
"manual_install": "pip install ane_transformers",
|
374
|
+
"package_size": "~10MB"
|
375
|
+
})
|
376
|
+
else:
|
377
|
+
# Generic module not found
|
378
|
+
module_match = re.search(r"No module named '([^']+)'", combined_output)
|
379
|
+
module_name = module_match.group(1) if module_match else "unknown"
|
380
|
+
error_details.update({
|
381
|
+
"error_category": "missing_module",
|
382
|
+
"error": f"Python module '{module_name}' not installed",
|
383
|
+
"install_command": f"uv pip install {module_name}",
|
384
|
+
"manual_install": f"pip install {module_name}"
|
385
|
+
})
|
386
|
+
elif "xcrun: error" in combined_output and "coremlc" in combined_output:
|
387
|
+
error_details.update({
|
388
|
+
"error_category": "missing_coremlc",
|
389
|
+
"error": "Core ML compiler (coremlc) not found - requires full Xcode installation",
|
390
|
+
"install_command": "Install Xcode from Mac App Store",
|
391
|
+
"note": "Command Line Tools alone are insufficient. Full Xcode provides coremlc for Core ML compilation.",
|
392
|
+
"alternative": "Models will work with Metal acceleration without Core ML compilation"
|
393
|
+
})
|
394
|
+
elif "xcrun: error" in combined_output:
|
395
|
+
error_details.update({
|
396
|
+
"error_category": "missing_xcode_tools",
|
397
|
+
"error": "Xcode Command Line Tools not installed or xcrun not available",
|
398
|
+
"install_command": "xcode-select --install",
|
399
|
+
"note": "Requires Xcode Command Line Tools"
|
400
|
+
})
|
401
|
+
elif "timeout" in combined_output.lower():
|
402
|
+
error_details.update({
|
403
|
+
"error_category": "conversion_timeout",
|
404
|
+
"error": "Core ML conversion timed out",
|
405
|
+
"suggestion": "Try with a smaller model or increase timeout"
|
406
|
+
})
|
407
|
+
else:
|
408
|
+
# Generic conversion failure
|
409
|
+
error_details.update({
|
410
|
+
"error_category": "conversion_failure",
|
411
|
+
"error": f"Core ML conversion failed",
|
412
|
+
"stderr": error_text[:500] if error_text else None, # Truncate long errors
|
413
|
+
"stdout": stdout_text[:500] if stdout_text else None
|
414
|
+
})
|
415
|
+
|
416
|
+
logger.error(f"Core ML conversion failed - Category: {error_details.get('error_category', 'unknown')}, Error: {error_text[:200]}")
|
417
|
+
return error_details
|
418
|
+
|
419
|
+
except subprocess.TimeoutExpired as e:
|
420
|
+
logger.error(f"Core ML conversion timed out after {e.timeout} seconds")
|
238
421
|
return {
|
239
422
|
"success": False,
|
240
|
-
"
|
423
|
+
"error_category": "timeout",
|
424
|
+
"error": f"Core ML conversion timed out after {e.timeout} seconds",
|
425
|
+
"suggestion": "Model conversion is taking too long. Try again or use a smaller model."
|
241
426
|
}
|
242
427
|
except Exception as e:
|
243
|
-
logger.error(f"
|
428
|
+
logger.error(f"Unexpected error during Core ML conversion: {e}")
|
244
429
|
return {
|
245
430
|
"success": False,
|
246
|
-
"
|
431
|
+
"error_category": "unexpected_error",
|
432
|
+
"error": str(e),
|
433
|
+
"error_type": type(e).__name__
|
247
434
|
}
|
248
435
|
|
249
436
|
|
@@ -0,0 +1,138 @@
|
|
1
|
+
"""Helper functions to get whisper.cpp version and capabilities."""
|
2
|
+
|
3
|
+
import subprocess
|
4
|
+
import re
|
5
|
+
from pathlib import Path
|
6
|
+
from typing import Dict, Any, Optional
|
7
|
+
|
8
|
+
|
9
|
+
def get_whisper_version_info() -> Dict[str, Any]:
|
10
|
+
"""Get version and capability information for whisper.cpp.
|
11
|
+
|
12
|
+
Returns:
|
13
|
+
Dict containing version, commit hash, Core ML support status, etc.
|
14
|
+
"""
|
15
|
+
info = {
|
16
|
+
"version": None,
|
17
|
+
"commit": None,
|
18
|
+
"coreml_supported": False,
|
19
|
+
"metal_supported": False,
|
20
|
+
"cuda_supported": False,
|
21
|
+
"build_type": None
|
22
|
+
}
|
23
|
+
|
24
|
+
# Find whisper-cli binary
|
25
|
+
whisper_dir = Path.home() / ".voicemode" / "services" / "whisper"
|
26
|
+
whisper_cli = whisper_dir / "build" / "bin" / "whisper-cli"
|
27
|
+
|
28
|
+
# Fallback to legacy location
|
29
|
+
if not whisper_cli.exists():
|
30
|
+
whisper_cli = whisper_dir / "main"
|
31
|
+
|
32
|
+
if not whisper_cli.exists():
|
33
|
+
return info
|
34
|
+
|
35
|
+
try:
|
36
|
+
# Get version from git if available
|
37
|
+
if (whisper_dir / ".git").exists():
|
38
|
+
result = subprocess.run(
|
39
|
+
["git", "describe", "--tags", "--always"],
|
40
|
+
cwd=whisper_dir,
|
41
|
+
capture_output=True,
|
42
|
+
text=True,
|
43
|
+
timeout=5
|
44
|
+
)
|
45
|
+
if result.returncode == 0:
|
46
|
+
info["version"] = result.stdout.strip()
|
47
|
+
|
48
|
+
# Get commit hash
|
49
|
+
result = subprocess.run(
|
50
|
+
["git", "rev-parse", "--short", "HEAD"],
|
51
|
+
cwd=whisper_dir,
|
52
|
+
capture_output=True,
|
53
|
+
text=True,
|
54
|
+
timeout=5
|
55
|
+
)
|
56
|
+
if result.returncode == 0:
|
57
|
+
info["commit"] = result.stdout.strip()
|
58
|
+
|
59
|
+
# Run whisper-cli to check capabilities
|
60
|
+
# Use a non-existent file to make it fail quickly but still show system info
|
61
|
+
result = subprocess.run(
|
62
|
+
[str(whisper_cli), "-h"],
|
63
|
+
capture_output=True,
|
64
|
+
text=True,
|
65
|
+
timeout=5
|
66
|
+
)
|
67
|
+
|
68
|
+
if result.returncode == 0:
|
69
|
+
output = result.stdout + result.stderr
|
70
|
+
|
71
|
+
# Check for Core ML support in help text or by running with dummy input
|
72
|
+
# Try running with minimal command to get system info
|
73
|
+
test_result = subprocess.run(
|
74
|
+
[str(whisper_cli), "--help"],
|
75
|
+
capture_output=True,
|
76
|
+
text=True,
|
77
|
+
timeout=5
|
78
|
+
)
|
79
|
+
|
80
|
+
# The system_info line shows what's compiled in
|
81
|
+
# We need to actually run it to see the capabilities
|
82
|
+
# Let's try with a non-existent model to fail fast but show system info
|
83
|
+
test_result = subprocess.run(
|
84
|
+
[str(whisper_cli), "-m", "nonexistent.bin"],
|
85
|
+
capture_output=True,
|
86
|
+
text=True,
|
87
|
+
timeout=5
|
88
|
+
)
|
89
|
+
|
90
|
+
test_output = test_result.stdout + test_result.stderr
|
91
|
+
|
92
|
+
# Parse system_info line for capabilities
|
93
|
+
if "COREML = 1" in test_output:
|
94
|
+
info["coreml_supported"] = True
|
95
|
+
elif "COREML = 0" in test_output:
|
96
|
+
info["coreml_supported"] = False
|
97
|
+
|
98
|
+
if "Metal" in test_output:
|
99
|
+
info["metal_supported"] = True
|
100
|
+
|
101
|
+
if "CUDA = 1" in test_output or "CUBLAS = 1" in test_output:
|
102
|
+
info["cuda_supported"] = True
|
103
|
+
|
104
|
+
# Check if this is a CMake or Make build
|
105
|
+
if (whisper_dir / "build" / "CMakeCache.txt").exists():
|
106
|
+
info["build_type"] = "CMake"
|
107
|
+
|
108
|
+
# Parse CMake cache for feature flags
|
109
|
+
with open(whisper_dir / "build" / "CMakeCache.txt") as f:
|
110
|
+
cmake_content = f.read()
|
111
|
+
if "WHISPER_COREML:BOOL=ON" in cmake_content:
|
112
|
+
info["coreml_supported"] = True
|
113
|
+
if "GGML_METAL:BOOL=ON" in cmake_content or "WHISPER_METAL:BOOL=ON" in cmake_content:
|
114
|
+
info["metal_supported"] = True
|
115
|
+
if "GGML_CUDA:BOOL=ON" in cmake_content or "WHISPER_CUDA:BOOL=ON" in cmake_content:
|
116
|
+
info["cuda_supported"] = True
|
117
|
+
else:
|
118
|
+
info["build_type"] = "Make"
|
119
|
+
|
120
|
+
except Exception as e:
|
121
|
+
# Silently handle errors
|
122
|
+
pass
|
123
|
+
|
124
|
+
return info
|
125
|
+
|
126
|
+
|
127
|
+
def check_coreml_model_exists(model_name: str) -> bool:
|
128
|
+
"""Check if a Core ML model exists for the given whisper model.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
model_name: Name of the whisper model (e.g., "large-v3-turbo")
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
True if Core ML model exists, False otherwise
|
135
|
+
"""
|
136
|
+
whisper_dir = Path.home() / ".voicemode" / "services" / "whisper"
|
137
|
+
coreml_model = whisper_dir / "models" / f"ggml-{model_name}-encoder.mlmodelc"
|
138
|
+
return coreml_model.exists()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: voice-mode
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.28.1
|
4
4
|
Summary: VoiceMode - Voice interaction capabilities for AI assistants (formerly voice-mcp)
|
5
5
|
Project-URL: Homepage, https://github.com/mbailey/voicemode
|
6
6
|
Project-URL: Repository, https://github.com/mbailey/voicemode
|
@@ -98,6 +98,10 @@ Natural voice conversations for AI assistants. Voice Mode brings human-like voic
|
|
98
98
|
1. **🎤 Computer with microphone and speakers** OR **☁️ LiveKit server** ([LiveKit Cloud](https://docs.livekit.io/home/cloud/) or [self-hosted](https://github.com/livekit/livekit))
|
99
99
|
2. **🔑 OpenAI API Key** (optional) - Voice Mode can install free, open-source transcription and text-to-speech services locally
|
100
100
|
|
101
|
+
**Optional for enhanced performance:**
|
102
|
+
|
103
|
+
- **🍎 Xcode** (macOS only) - Required for Core ML acceleration of Whisper models (2-3x faster inference). Install from [Mac App Store](https://apps.apple.com/app/xcode/id497799835) then run `sudo xcode-select -s /Applications/Xcode.app/Contents/Developer`
|
104
|
+
|
101
105
|
## Quick Start
|
102
106
|
|
103
107
|
> 📖 **Using a different tool?** See our [Integration Guides](docs/integrations/README.md) for Cursor, VS Code, Gemini CLI, and more!
|