mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix: avoid local STT crash on Apple Silicon
This commit is contained in:
parent
12c39830f0
commit
7edf2d5196
3 changed files with 100 additions and 1 deletions
|
|
@ -92,7 +92,7 @@ voice = [
|
||||||
# so keep it out of the base install for source-build packagers like Homebrew.
|
# so keep it out of the base install for source-build packagers like Homebrew.
|
||||||
"faster-whisper==1.2.1",
|
"faster-whisper==1.2.1",
|
||||||
"sounddevice==0.5.5",
|
"sounddevice==0.5.5",
|
||||||
"numpy==2.4.3",
|
"numpy<2",
|
||||||
]
|
]
|
||||||
pty = [
|
pty = [
|
||||||
"ptyprocess==0.7.0; sys_platform != 'win32'",
|
"ptyprocess==0.7.0; sys_platform != 'win32'",
|
||||||
|
|
|
||||||
|
|
@ -509,6 +509,51 @@ class TestTranscribeLocalExtended:
|
||||||
assert result["success"] is True
|
assert result["success"] is True
|
||||||
assert result["transcript"] == "Hello world"
|
assert result["transcript"] == "Hello world"
|
||||||
|
|
||||||
|
def test_apple_silicon_forces_cpu_without_auto_probe(self, tmp_path):
|
||||||
|
"""Apple Silicon/Rosetta should skip device='auto' to avoid SIGABRT."""
|
||||||
|
audio = tmp_path / "test.ogg"
|
||||||
|
audio.write_bytes(b"fake")
|
||||||
|
|
||||||
|
seg = MagicMock()
|
||||||
|
seg.text = "safe"
|
||||||
|
info = MagicMock()
|
||||||
|
info.language = "en"
|
||||||
|
info.duration = 1.0
|
||||||
|
cpu_model = MagicMock()
|
||||||
|
cpu_model.transcribe.return_value = ([seg], info)
|
||||||
|
mock_whisper_cls = MagicMock(return_value=cpu_model)
|
||||||
|
|
||||||
|
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
|
||||||
|
patch("tools.transcription_tools._should_force_faster_whisper_cpu", return_value=True), \
|
||||||
|
patch("faster_whisper.WhisperModel", mock_whisper_cls), \
|
||||||
|
patch("tools.transcription_tools._local_model", None), \
|
||||||
|
patch("tools.transcription_tools._local_model_name", None):
|
||||||
|
from tools.transcription_tools import _transcribe_local
|
||||||
|
result = _transcribe_local(str(audio), "base")
|
||||||
|
|
||||||
|
assert result["success"] is True
|
||||||
|
assert result["transcript"] == "safe"
|
||||||
|
mock_whisper_cls.assert_called_once_with("base", device="cpu", compute_type="int8")
|
||||||
|
|
||||||
|
def test_force_cpu_detects_rosetta_on_apple_silicon(self):
|
||||||
|
from tools.transcription_tools import _should_force_faster_whisper_cpu
|
||||||
|
|
||||||
|
with patch("tools.transcription_tools.platform.system", return_value="Darwin"), \
|
||||||
|
patch("tools.transcription_tools.platform.machine", return_value="x86_64"), \
|
||||||
|
patch("tools.transcription_tools._sysctl_value", side_effect=lambda key: {
|
||||||
|
"sysctl.proc_translated": "1",
|
||||||
|
"hw.optional.arm64": "1",
|
||||||
|
}.get(key, "")):
|
||||||
|
assert _should_force_faster_whisper_cpu() is True
|
||||||
|
|
||||||
|
def test_force_cpu_false_on_intel_macos(self):
|
||||||
|
from tools.transcription_tools import _should_force_faster_whisper_cpu
|
||||||
|
|
||||||
|
with patch("tools.transcription_tools.platform.system", return_value="Darwin"), \
|
||||||
|
patch("tools.transcription_tools.platform.machine", return_value="x86_64"), \
|
||||||
|
patch("tools.transcription_tools._sysctl_value", return_value="0"):
|
||||||
|
assert _should_force_faster_whisper_cpu() is False
|
||||||
|
|
||||||
def test_load_time_cuda_lib_failure_falls_back_to_cpu(self, tmp_path):
|
def test_load_time_cuda_lib_failure_falls_back_to_cpu(self, tmp_path):
|
||||||
"""Missing libcublas at load time → reload on CPU, succeed."""
|
"""Missing libcublas at load time → reload on CPU, succeed."""
|
||||||
audio = tmp_path / "test.ogg"
|
audio = tmp_path / "test.ogg"
|
||||||
|
|
@ -532,6 +577,7 @@ class TestTranscribeLocalExtended:
|
||||||
return cpu_model
|
return cpu_model
|
||||||
|
|
||||||
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
|
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
|
||||||
|
patch("tools.transcription_tools._should_force_faster_whisper_cpu", return_value=False), \
|
||||||
patch("faster_whisper.WhisperModel", side_effect=fake_whisper), \
|
patch("faster_whisper.WhisperModel", side_effect=fake_whisper), \
|
||||||
patch("tools.transcription_tools._local_model", None), \
|
patch("tools.transcription_tools._local_model", None), \
|
||||||
patch("tools.transcription_tools._local_model_name", None):
|
patch("tools.transcription_tools._local_model_name", None):
|
||||||
|
|
@ -570,6 +616,7 @@ class TestTranscribeLocalExtended:
|
||||||
return models.pop(0)
|
return models.pop(0)
|
||||||
|
|
||||||
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
|
with patch("tools.transcription_tools._HAS_FASTER_WHISPER", True), \
|
||||||
|
patch("tools.transcription_tools._should_force_faster_whisper_cpu", return_value=False), \
|
||||||
patch("faster_whisper.WhisperModel", side_effect=fake_whisper), \
|
patch("faster_whisper.WhisperModel", side_effect=fake_whisper), \
|
||||||
patch("tools.transcription_tools._local_model", None), \
|
patch("tools.transcription_tools._local_model", None), \
|
||||||
patch("tools.transcription_tools._local_model_name", None):
|
patch("tools.transcription_tools._local_model_name", None):
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ Usage::
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import platform
|
||||||
import shlex
|
import shlex
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
@ -371,6 +372,42 @@ def _looks_like_cuda_lib_error(exc: BaseException) -> bool:
|
||||||
return any(marker in msg for marker in _CUDA_LIB_ERROR_MARKERS)
|
return any(marker in msg for marker in _CUDA_LIB_ERROR_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
|
def _sysctl_value(name: str) -> str:
|
||||||
|
"""Return a sysctl value, or an empty string when unavailable."""
|
||||||
|
try:
|
||||||
|
return subprocess.check_output(
|
||||||
|
["/usr/sbin/sysctl", "-n", name],
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
text=True,
|
||||||
|
timeout=2,
|
||||||
|
).strip()
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _should_force_faster_whisper_cpu() -> bool:
|
||||||
|
"""Avoid faster-whisper device autodetection paths known to hard-abort.
|
||||||
|
|
||||||
|
On Apple Silicon, especially when Python is running as x86_64 under
|
||||||
|
Rosetta, ctranslate2's ``device=\"auto\"`` path can abort inside native
|
||||||
|
code before Python can catch an exception. Force CPU so local STT remains
|
||||||
|
reliable for gateway voice messages.
|
||||||
|
"""
|
||||||
|
if platform.system() != "Darwin":
|
||||||
|
return False
|
||||||
|
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
if machine in {"arm64", "aarch64"}:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Under Rosetta, platform.machine() reports x86_64. sysctl.proc_translated
|
||||||
|
# tells us this process is translated, while hw.optional.arm64 distinguishes
|
||||||
|
# Apple Silicon hosts from Intel Macs.
|
||||||
|
if _sysctl_value("sysctl.proc_translated") == "1":
|
||||||
|
return True
|
||||||
|
return _sysctl_value("hw.optional.arm64") == "1"
|
||||||
|
|
||||||
|
|
||||||
def _load_local_whisper_model(model_name: str):
|
def _load_local_whisper_model(model_name: str):
|
||||||
"""Load faster-whisper with graceful CUDA → CPU fallback.
|
"""Load faster-whisper with graceful CUDA → CPU fallback.
|
||||||
|
|
||||||
|
|
@ -384,7 +421,22 @@ def _load_local_whisper_model(model_name: str):
|
||||||
We try ``auto`` first (fast CUDA path when it works), and on any CUDA
|
We try ``auto`` first (fast CUDA path when it works), and on any CUDA
|
||||||
library load failure fall back to CPU + int8.
|
library load failure fall back to CPU + int8.
|
||||||
"""
|
"""
|
||||||
|
force_cpu = _should_force_faster_whisper_cpu()
|
||||||
|
if force_cpu:
|
||||||
|
# Importing ctranslate2/faster-whisper itself can abort on some
|
||||||
|
# Apple Silicon/Rosetta installs because multiple Intel OpenMP runtimes
|
||||||
|
# are already loaded. Set this before importing faster_whisper so the
|
||||||
|
# gateway survives, then keep inference on CPU to avoid device probing.
|
||||||
|
os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
|
||||||
|
|
||||||
from faster_whisper import WhisperModel
|
from faster_whisper import WhisperModel
|
||||||
|
if force_cpu:
|
||||||
|
logger.info(
|
||||||
|
"Apple Silicon/Rosetta detected — loading faster-whisper on CPU "
|
||||||
|
"(int8) to avoid native device autodetection crashes"
|
||||||
|
)
|
||||||
|
return WhisperModel(model_name, device="cpu", compute_type="int8")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return WhisperModel(model_name, device="auto", compute_type="auto")
|
return WhisperModel(model_name, device="auto", compute_type="auto")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue