fix(installer): pin sherpa-onnx-core so Parakeet installs import (#1000)

sherpa-onnx 1.13.7 wheels require sherpa-onnx-core, which ships
libonnxruntime.so, but the armv7l wheels omit that metadata and uv.lock
recorded no dependency. install.sh installs the export with --no-deps, so
sherpa_onnx failed to import, Settings hid Parakeet and the installer
quietly switched config.json to whisper.cpp while still reporting Parakeet.

List sherpa-onnx-core in the parakeet extra, verify engine imports right
after installing them, and keep SELECTED_ENGINE in sync when the config
is repaired so the summary names the engine actually configured.

Fixes #973
This commit is contained in:
Jatin K Malik
2026-10-20 23:58:38 -07:00
committed by GitHub
parent cf23e9d82d
commit 566a228858
7 changed files with 154 additions and 16 deletions
+21 -1
View File
@@ -1882,6 +1882,22 @@ venv_can_import() {
"$VENV_DIR/bin/python" -c "import $1" >/dev/null 2>&1
}
engine_import_error() {
local ENGINE="$1"
local MODULE
MODULE=$(engine_import_module "$ENGINE")
[ -n "$MODULE" ] || return 1
local IMPORT_ERROR
if IMPORT_ERROR=$("$VENV_DIR/bin/python" -c "import $MODULE" 2>&1); then
return 0
fi
print_error "$ENGINE installed but \`import $MODULE\` failed:"
print_error "$(printf '%s\n' "$IMPORT_ERROR" | tail -n 1)"
return 1
}
# Point config.json at engine $2. Non-zero if it could not be rewritten.
set_configured_engine() {
"$VENV_DIR/bin/python" - "$1" "$2" <<'PY' 2>/dev/null
@@ -2384,6 +2400,7 @@ FALLBACK_CONFIG
print_error "Failed to install the vosk engine"
return 1
}
engine_import_error vosk || return 1
# Create config with vosk as default
local VOSK_CONFIG_FILE="$CONFIG_DIR/config.json"
@@ -2431,6 +2448,7 @@ VOSK_CONFIG
print_error "Failed to install the parakeet engine"
return 1
}
engine_import_error parakeet || return 1
# config_manager merges the remaining defaults, but a file with no
# shortcuts section reads as a pre-push-to-talk config and gets
@@ -2523,7 +2541,8 @@ REMOTE_CONFIG
print_info "Installing Faster-Whisper engine..."
print_info "This engine uses CTranslate2 for fast CPU inference."
if pip_install_extras_skip_pygobject "$PIP_LOG_FILE" faster_whisper; then
if pip_install_extras_skip_pygobject "$PIP_LOG_FILE" faster_whisper &&
engine_import_error faster_whisper; then
mkdir -p "$CONFIG_DIR"
if [ ! -f "$CONFIG_DIR/config.json" ]; then
cat > "$CONFIG_DIR/config.json" << 'FASTER_WHISPER_CONFIG'
@@ -2770,6 +2789,7 @@ PY
[ -n "$MODULE_FOR_CANDIDATE" ] || continue
venv_can_import "$MODULE_FOR_CANDIDATE" || continue
if set_configured_engine "$CONFIG_FILE" "$CANDIDATE"; then
SELECTED_ENGINE="$CANDIDATE"
print_success "Switched $CONFIG_FILE to the $CANDIDATE engine."
print_info "Reinstall $(engine_pip_name "$CONFIGURED_ENGINE") and switch back in Settings if you want it."
return 0
+3
View File
@@ -59,6 +59,9 @@ whisper = [
]
parakeet = [
"sherpa-onnx>=1.12.0,<2",
# Match the locked sherpa-onnx version; bump both together. armv7l wheels omit this
# dependency metadata, and the export test enforces the matching core pin.
"sherpa-onnx-core==1.13.7; platform_machine != 'armv7l'",
]
faster_whisper = [
"faster-whisper>=1.0.0,<2",
+15
View File
@@ -508,6 +508,21 @@ sherpa-onnx==1.13.7 \
--hash=sha256:fce7cfab223d6bee0a58fa940b5057e2f3cf63613928b88e09ac99b477e40be0 \
--hash=sha256:fed61cc23dba8f212eeb2723d2e90108d6de95734c5ae427aeafdb10f09ec184
# via vocalinux
sherpa-onnx-core==1.13.7 ; platform_machine != 'armv7l' \
--hash=sha256:07de22f590299a68d9b5c985264f380579600a831ee95510ab12f4068800d08c \
--hash=sha256:861c1e18fbf9bd2c725992c5a1797efe1b934b7df7bb18986d1fdc60e91290c9 \
--hash=sha256:9ee6d4f05c14eb961370ba794f363088407465bf2006b73ab67508abd80be033 \
--hash=sha256:b191e79d4952199bd11ce05c5b29c5efb08ed43a5d96839202e8ab603ab96278 \
--hash=sha256:befe8193f02ed7935875acf540b8a554da0a89060965b32c1364f37c4ca7f3b3 \
--hash=sha256:c53a930543f3a0f1063956fd3d01081acd6cb66f46c9b1f544b4393d6cbc1893 \
--hash=sha256:cbcb78ef8a3bb74acf0b9d0a8715e9b857491be8e2b1e8f589f09ecf3d2f2a0b \
--hash=sha256:cf17e20cada234df952f71a0e9412422c557e08246a6759b8aa6ea4bbd215a5a \
--hash=sha256:d0131f93e8fabca773701e28301d81dff4f7445ba59e280fca319c585bac850a \
--hash=sha256:e11c2dca5526ff473bd5cf94f40da7ff7075ec4c44f3464cc1bceffdc6f5e487 \
--hash=sha256:e1e6333ecde50c69b8a43feff6eedf1033ec2a3fe58009506572bd717fc78e42 \
--hash=sha256:e3066e94c24c898c5794e08507f955e14bc904299bac80f357d0bcb7d93e3825 \
--hash=sha256:f48029e0c79b4cc654076806cbf62094a51dcd26c6bd1ce17db3bad03aff02b1
# via vocalinux
six==1.17.0 \
--hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
--hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
+2
View File
@@ -314,6 +314,8 @@ def test_every_selectable_engine_reaches_optdepends() -> None:
dist = re.split(r"[<>=!~\[; ]", spec, maxsplit=1)[0].strip().lower()
if dist in {"torch", "torchaudio"}:
continue # pulled in by python-openai-whisper, not named alone
if dist == "sherpa-onnx-core":
continue # Arch's python-sherpa-onnx links the system onnxruntime instead
arch_name = f"python-{dist.replace('_', '-')}"
if arch_name not in optdepends:
missing.append(f"{extra} -> {arch_name}")
+30
View File
@@ -53,6 +53,15 @@ def _exported_names(path: Path) -> set[str]:
return names
def _exported_versions(path: Path, names: set[str]) -> dict[str, str]:
versions = {}
for line in path.read_text(encoding="utf-8").splitlines():
match = re.match(r"^([A-Za-z0-9._-]+)==([^ ;\\]+)", line)
if match and match.group(1).lower() in names:
versions[match.group(1).lower()] = match.group(2)
return versions
def _extras() -> dict[str, list[str]]:
return _pyproject()["project"]["optional-dependencies"]
@@ -321,6 +330,27 @@ def test_every_extra_reaches_its_own_export() -> None:
assert not missing, "re-run `just lock`:\n" + "\n".join(missing)
def test_parakeet_extra_declares_sherpa_onnx_core() -> None:
"""The lock needs an explicit dependency when armv7l wheel metadata omits it."""
assert "sherpa-onnx-core" in _requirement_names(_extras()["parakeet"])
def test_parakeet_export_pins_sherpa_onnx_core_to_sherpa_version() -> None:
"""The core library version must match the locked sherpa-onnx version."""
versions = _exported_versions(
REQUIREMENTS / "parakeet.txt", {"sherpa-onnx", "sherpa-onnx-core"}
)
assert versions["sherpa-onnx-core"] == versions["sherpa-onnx"]
def test_parakeet_lock_pins_sherpa_onnx_core_to_sherpa_version() -> None:
"""The lock keeps sherpa-onnx-core at the engine's version."""
with (REPO_ROOT / "uv.lock").open("rb") as handle:
packages = tomllib.load(handle)["package"]
versions = {package["name"]: package["version"] for package in packages if "version" in package}
assert versions["sherpa-onnx-core"] == versions["sherpa-onnx"]
def test_every_extra_the_installer_installs_is_pinnable() -> None:
"""install.sh is the path that hands users unpinned ranges today (2.3).
+61 -15
View File
@@ -5,6 +5,8 @@ cannot import — vosk is an optional extra since #705. Vocalinux then dies at
startup with "No module named 'vosk'" instead of the installer saying so.
"""
from __future__ import annotations
import json
import subprocess
from pathlib import Path
@@ -21,13 +23,13 @@ print_success() { echo "SUCCESS: $*"; }
"""
def _run(script: str) -> subprocess.CompletedProcess:
def _run(script: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["bash", "-c", PRELUDE + script], capture_output=True, text=True, timeout=30
)
def _venv_python(venv_dir: Path, importable: "set[str] | None" = None) -> Path:
def _venv_python(venv_dir: Path, importable: set[str] | None = None) -> Path:
"""A stub venv interpreter: real python3, with chosen imports forced.
``importable`` names the engine modules that succeed; every other engine
@@ -40,7 +42,9 @@ def _venv_python(venv_dir: Path, importable: "set[str] | None" = None) -> Path:
branches = "".join(
'if [[ "$*" == *"import {module}"* ]]; then\n'
" exit {code}\n"
" if [ {code} -eq 0 ]; then exit 0; fi\n"
" echo \"ModuleNotFoundError: No module named '{module}'\" >&2\n"
" exit 1\n"
"fi\n".format(module=module, code=0 if module in ok else 1)
for module in sorted(modules, key=len, reverse=True)
)
@@ -62,21 +66,51 @@ def _config(config_dir: Path, engine: str) -> Path:
def _check(
config_dir: Path, venv_dir: Path, selected_engine: str = ""
) -> subprocess.CompletedProcess:
) -> subprocess.CompletedProcess[str]:
script = f"""
CONFIG_DIR="{config_dir}"
VENV_DIR="{venv_dir}"
SELECTED_ENGINE="{selected_engine}"
for fn in engine_import_module engine_pip_name venv_can_import set_configured_engine verify_configured_engine; do
for fn in engine_import_module engine_pip_name venv_can_import \
set_configured_engine verify_configured_engine; do
source <(sed -n "/^$fn() {{$/,/^}}$/p" "{INSTALL_SH}")
done
verify_configured_engine
echo "EXIT=$?"
echo "SELECTED=$SELECTED_ENGINE"
"""
return _run(script)
def _check_engine_import(venv_dir: Path, engine: str) -> subprocess.CompletedProcess[str]:
script = f"""
VENV_DIR="{venv_dir}"
for fn in engine_import_module engine_import_error; do
source <(sed -n "/^$fn() {{$/,/^}}$/p" "{INSTALL_SH}")
done
engine_import_error "{engine}"
echo "EXIT=$?"
"""
return _run(script)
def test_repairs_a_config_naming_an_uninstallable_engine(tmp_path):
def test_engine_import_error_reports_success_and_failure(tmp_path: Path) -> None:
venv = tmp_path / "venv"
_venv_python(venv, importable={"sherpa_onnx"})
imported = _check_engine_import(venv, "parakeet")
assert "EXIT=0" in imported.stdout
_venv_python(venv, importable=set())
failed = _check_engine_import(venv, "parakeet")
assert "EXIT=1" in failed.stdout
assert "ERROR: parakeet installed but `import sherpa_onnx` failed:" in failed.stdout
assert "ERROR: ModuleNotFoundError: No module named 'sherpa_onnx'" in failed.stdout
def test_repairs_a_config_naming_an_uninstallable_engine(tmp_path: Path) -> None:
"""Don't leave a config the app cannot start with."""
venv = tmp_path / "venv"
_venv_python(venv, importable={"pywhispercpp.model"})
@@ -89,7 +123,7 @@ def test_repairs_a_config_naming_an_uninstallable_engine(tmp_path):
assert "Switched" in result.stdout
def test_repairs_a_leftover_whisper_engine_too(tmp_path):
def test_repairs_a_leftover_whisper_engine_too(tmp_path: Path) -> None:
"""The check was vosk-only; whisper is an optional extra as well."""
venv = tmp_path / "venv"
_venv_python(venv, importable={"pywhispercpp.model"})
@@ -101,7 +135,7 @@ def test_repairs_a_leftover_whisper_engine_too(tmp_path):
assert _engine_of(config) == "whisper_cpp"
def test_fails_when_no_engine_works(tmp_path):
def test_fails_when_no_engine_works(tmp_path: Path) -> None:
"""Nothing to fall back to: the install must not report success."""
venv = tmp_path / "venv"
_venv_python(venv, importable=set())
@@ -115,7 +149,7 @@ def test_fails_when_no_engine_works(tmp_path):
assert _engine_of(config) == "vosk"
def test_leaves_a_working_engine_alone(tmp_path):
def test_leaves_a_working_engine_alone(tmp_path: Path) -> None:
venv = tmp_path / "venv"
_venv_python(venv, importable={"vosk"})
config = _config(tmp_path / "config", "vosk")
@@ -127,7 +161,7 @@ def test_leaves_a_working_engine_alone(tmp_path):
assert "WARNING" not in result.stdout
def test_ignores_engines_that_need_no_extra(tmp_path):
def test_ignores_engines_that_need_no_extra(tmp_path: Path) -> None:
venv = tmp_path / "venv"
_venv_python(venv, importable=set())
config = _config(tmp_path / "config", "remote_api")
@@ -138,7 +172,7 @@ def test_ignores_engines_that_need_no_extra(tmp_path):
assert _engine_of(config) == "remote_api"
def test_silent_without_a_config_file(tmp_path):
def test_silent_without_a_config_file(tmp_path: Path) -> None:
venv = tmp_path / "venv"
_venv_python(venv, importable=set())
@@ -147,7 +181,7 @@ def test_silent_without_a_config_file(tmp_path):
assert "EXIT=0" in result.stdout
def test_repairs_whisper_cpp_to_vosk_when_that_is_what_works(tmp_path):
def test_repairs_whisper_cpp_to_vosk_when_that_is_what_works(tmp_path: Path) -> None:
"""Reinstall: config still says whisper_cpp, pywhispercpp is gone, vosk is in."""
venv = tmp_path / "venv"
_venv_python(venv, importable={"vosk"})
@@ -160,7 +194,7 @@ def test_repairs_whisper_cpp_to_vosk_when_that_is_what_works(tmp_path):
assert "Switched" in result.stdout
def test_honors_selected_engine_ahead_of_the_default_fallback(tmp_path):
def test_honors_selected_engine_ahead_of_the_default_fallback(tmp_path: Path) -> None:
"""SELECTED_ENGINE is tried before the default vosk/whisper extras."""
venv = tmp_path / "venv"
_venv_python(venv, importable={"vosk", "whisper"})
@@ -172,14 +206,26 @@ def test_honors_selected_engine_ahead_of_the_default_fallback(tmp_path):
assert _engine_of(config) == "whisper"
def test_vosk_fallback_rewrites_an_existing_config_file():
def test_selected_engine_tracks_a_parakeet_config_repair(tmp_path: Path) -> None:
venv = tmp_path / "venv"
_venv_python(venv, importable={"pywhispercpp.model"})
config = _config(tmp_path / "config", "parakeet")
result = _check(tmp_path / "config", venv, selected_engine="parakeet")
assert "EXIT=0" in result.stdout
assert "SELECTED=whisper_cpp" in result.stdout
assert _engine_of(config) == "whisper_cpp"
def test_vosk_fallback_rewrites_an_existing_config_file() -> None:
"""The fallback used to write vosk only when config.json was missing."""
text = INSTALL_SH.read_text(encoding="utf-8")
assert 'set_configured_engine "$FALLBACK_VOSK_CONFIG" "vosk"' in text
assert '[ ! -f "$FALLBACK_VOSK_CONFIG" ]' not in text
def test_unreadable_config_is_not_treated_as_an_engine(tmp_path):
def test_unreadable_config_is_not_treated_as_an_engine(tmp_path: Path) -> None:
venv = tmp_path / "venv"
_venv_python(venv, importable=set())
config_dir = tmp_path / "config"
Generated
+22
View File
@@ -2501,6 +2501,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/07/58fb06acf48e3040ee9aba95a7330f0804bf5ec8868ae49e90872b4bf979/sherpa_onnx-1.13.7-cp314-cp314-win_arm64.whl", hash = "sha256:52ca4392620bb4a0cb30373f245861107516c8831bface1c82c17a9e34f3f5c9", size = 2305339, upload-time = "2026-09-01T06:05:03.446Z" },
]
[[package]]
name = "sherpa-onnx-core"
version = "1.13.7"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/ce/16f5cebe925b2e7b08821ed966969382b2bc37a918de2ad402e8ab8a6eae/sherpa_onnx_core-1.13.7-py3-none-android_24_arm64_v8a.whl", hash = "sha256:e11c2dca5526ff473bd5cf94f40da7ff7075ec4c44f3464cc1bceffdc6f5e487", size = 9710257, upload-time = "2026-09-04T14:54:00.278Z" },
{ url = "https://files.pythonhosted.org/packages/e1/32/a90abb898d4cc311932c904e6088d1c762eacabdc9356f08e8cdf6b62e6f/sherpa_onnx_core-1.13.7-py3-none-android_24_armeabi_v7a.whl", hash = "sha256:9ee6d4f05c14eb961370ba794f363088407465bf2006b73ab67508abd80be033", size = 9124693, upload-time = "2026-09-05T22:26:30.231Z" },
{ url = "https://files.pythonhosted.org/packages/47/32/4b43de282aa643baec523c833e183a4dad833d0aa442770f01d9fc907307/sherpa_onnx_core-1.13.7-py3-none-android_24_x86.whl", hash = "sha256:f48029e0c79b4cc654076806cbf62094a51dcd26c6bd1ce17db3bad03aff02b1", size = 11105887, upload-time = "2026-09-05T22:32:49.997Z" },
{ url = "https://files.pythonhosted.org/packages/8a/6f/e1aef05ee2010dffb389fcbdc96fe849c8dab1996f7b6ec7abfafde40b95/sherpa_onnx_core-1.13.7-py3-none-android_24_x86_64.whl", hash = "sha256:e1e6333ecde50c69b8a43feff6eedf1033ec2a3fe58009506572bd717fc78e42", size = 10863620, upload-time = "2026-09-05T16:43:44.314Z" },
{ url = "https://files.pythonhosted.org/packages/d0/04/0348c408b99ab5bd490f70cfb5e40eb90c1ad269a17380400f9f345bd530/sherpa_onnx_core-1.13.7-py3-none-macosx_10_15_universal2.whl", hash = "sha256:d0131f93e8fabca773701e28301d81dff4f7445ba59e280fca319c585bac850a", size = 20329436, upload-time = "2026-09-01T05:33:47.588Z" },
{ url = "https://files.pythonhosted.org/packages/ca/08/49bcf63aa98d8d94cc234398d7670447b3562796662f88dc3f28e43a2f80/sherpa_onnx_core-1.13.7-py3-none-macosx_10_15_x86_64.whl", hash = "sha256:c53a930543f3a0f1063956fd3d01081acd6cb66f46c9b1f544b4393d6cbc1893", size = 10835919, upload-time = "2026-09-01T05:04:07.52Z" },
{ url = "https://files.pythonhosted.org/packages/26/0b/e03cdda14781c32f1f84b86c8307884a1a0afe89c563def821a9a2b466e1/sherpa_onnx_core-1.13.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b191e79d4952199bd11ce05c5b29c5efb08ed43a5d96839202e8ab603ab96278", size = 9523627, upload-time = "2026-09-01T05:13:35.625Z" },
{ url = "https://files.pythonhosted.org/packages/b0/f3/edb7082cd721ce801bf8bb537a08c5c3771ad9d0b27ae4c943d23a1becdb/sherpa_onnx_core-1.13.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:861c1e18fbf9bd2c725992c5a1797efe1b934b7df7bb18986d1fdc60e91290c9", size = 13058938, upload-time = "2026-09-01T05:04:11.848Z" },
{ url = "https://files.pythonhosted.org/packages/f6/b8/7176b23789f9f1814368b2faee2bf9ed4c16cb9e55498517a733114bd24d/sherpa_onnx_core-1.13.7-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e3066e94c24c898c5794e08507f955e14bc904299bac80f357d0bcb7d93e3825", size = 10373118, upload-time = "2026-09-01T05:12:24.901Z" },
{ url = "https://files.pythonhosted.org/packages/cd/54/8b34f96b48e790c69f1b6c0a42d12b82e97e401bd1259b861f97f4b40752/sherpa_onnx_core-1.13.7-py3-none-manylinux_2_35_armv7l.whl", hash = "sha256:cf17e20cada234df952f71a0e9412422c557e08246a6759b8aa6ea4bbd215a5a", size = 9922252, upload-time = "2026-09-01T06:44:45.369Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6c/ff34618f69bb1671f86056b9d1856b13be8376823a382353498f96c423ce/sherpa_onnx_core-1.13.7-py3-none-win32.whl", hash = "sha256:07de22f590299a68d9b5c985264f380579600a831ee95510ab12f4068800d08c", size = 14530298, upload-time = "2026-09-01T05:13:37.064Z" },
{ url = "https://files.pythonhosted.org/packages/20/87/2ac78c15d4b6bc8ba663b4fef8a9d01ef32a09c336b828d27af3e4962116/sherpa_onnx_core-1.13.7-py3-none-win_amd64.whl", hash = "sha256:cbcb78ef8a3bb74acf0b9d0a8715e9b857491be8e2b1e8f589f09ecf3d2f2a0b", size = 16526006, upload-time = "2026-09-01T05:30:51.329Z" },
{ url = "https://files.pythonhosted.org/packages/44/0e/77f9f9243245a865854b174d65eae52e7805cad885f8328f0eb1774fc6a7/sherpa_onnx_core-1.13.7-py3-none-win_arm64.whl", hash = "sha256:befe8193f02ed7935875acf540b8a554da0a89060965b32c1364f37c4ca7f3b3", size = 15689427, upload-time = "2026-09-01T05:25:56.119Z" },
]
[[package]]
name = "six"
version = "1.17.0"
@@ -2980,6 +3000,7 @@ faster-whisper = [
]
parakeet = [
{ name = "sherpa-onnx" },
{ name = "sherpa-onnx-core", marker = "platform_machine != 'armv7l'" },
]
vad = [
{ name = "onnxruntime" },
@@ -3032,6 +3053,7 @@ requires-dist = [
{ name = "pyyaml", marker = "extra == 'dev'", specifier = ">=6.0,<7" },
{ name = "requests", specifier = ">=2.28.0" },
{ name = "sherpa-onnx", marker = "extra == 'parakeet'", specifier = ">=1.12.0,<2" },
{ name = "sherpa-onnx-core", marker = "platform_machine != 'armv7l' and extra == 'parakeet'", specifier = "==1.13.7" },
{ name = "sphinx", marker = "extra == 'docs'", specifier = ">=6.0.0" },
{ name = "sphinx-rtd-theme", marker = "extra == 'docs'", specifier = ">=1.2.0" },
{ name = "torch", marker = "extra == 'whisper'", specifier = ">=2.13.0" },