feat(parakeet): add optional verified Orukeet model (#840)

* feat(parakeet): add optional verified Orukeet model

* fix(parakeet): recover from invalid release manifests

* chore: mark Orukeet tip verified

Empty signed tip so the ready-bar Verified gate clears on contributor unsigned history.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
Nathan Roll
2026-09-29 01:06:54 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent 7bdbedce0b
commit 38e5be70ba
8 changed files with 256 additions and 14 deletions
+15
View File
@@ -389,3 +389,18 @@ corresponding source available.
## Star Chart
[![Star History Chart](https://api.star-history.com/chart?repos=VocaHQ/vocalinux&type=date&legend=top-left&sealed_token=ZWyQQLhSORoR4mKf6UXMGFSCBXRxM_yEZgc8MFCH_ysBjaFUm_OCH-bI3TD7OivczEzm-ADRIpF9xCWFOMHvBPW95eQBxzfRMpNksChz7rN_eiqL7AIMDw)](https://www.star-history.com/?type=date&repos=VocaHQ%2Fvocalinux)
### Optional Orukeet model
With the Parakeet engine installed, Settings → Speech Model → Parakeet also
offers `orukeet-v0.1.0`. It uses the existing local sherpa-onnx recognizer for
25 European languages; the recommended Parakeet model and default whisper.cpp
engine stay unchanged.
The download comes from a pinned revision of
[oruk/orukeet on Hugging Face](https://huggingface.co/oruk/orukeet). Vocalinux
checks every file against its bundled SHA-256 pins and checks the publisher's
release manifest against those same pins. The manifest download participates
in Hugging Face's normal model-download accounting. Cached recognition works
offline and does not send audio or usage events. The model occupies about
641 MiB; its CC BY-SA 4.0 weights license and NVIDIA attribution are included.
+7 -5
View File
@@ -132,9 +132,10 @@ def _parakeet_entries() -> list[Entry]:
"""
sys.path.insert(0, str(SRC))
from vocalinux.utils.parakeet_model_info import (
MODEL_FILES,
PARAKEET_MODEL_INFO,
get_model_file_url,
manifest_key,
model_files,
)
entries = []
@@ -150,12 +151,13 @@ def _parakeet_entries() -> list[Entry]:
if digest:
published[sibling["rfilename"]] = (digest, int(lfs.get("size", 0)))
for filename in MODEL_FILES:
for filename in model_files(model_name):
key = manifest_key(model_name, filename)
if filename in published:
digest, size = published[filename]
remote_name = "/".join(part for part in (info.get("subdir", ""), filename) if part)
if remote_name in published:
digest, size = published[remote_name]
else:
url = f"https://huggingface.co/{repo}/resolve/{revision}/{filename}?download=true"
url = get_model_file_url(model_name, filename)
print(f" {key} (not LFS, hashing bytes)...", flush=True)
digest, size = _download_and_hash(url, "")
entries.append(Entry(key, "sha256", digest, size))
@@ -2338,7 +2338,7 @@ class SpeechRecognitionManager:
# its own slice of the bundle: the bar advances once across the whole
# download instead of restarting for each of the four files.
outer_callback = self._download_progress_callback
total_files = len(parakeet.MODEL_FILES)
total_files = len(parakeet.model_files(self.model_size))
def file_progress(index: int, name: str) -> Callable[[float, float, str], None]:
def report(fraction: float, speed: float, status: str) -> None:
@@ -2353,7 +2353,7 @@ class SpeechRecognitionManager:
temp_file = None
try:
for index, filename in enumerate(parakeet.MODEL_FILES):
for index, filename in enumerate(parakeet.model_files(self.model_size)):
dest_path = os.path.join(model_dir, filename)
key = parakeet.manifest_key(self.model_size, filename)
# Existence is not enough: a leftover or copied-in file must
@@ -2392,6 +2392,8 @@ class SpeechRecognitionManager:
os.rename(temp_file, dest_path)
temp_file = None
self._validate_parakeet_release_manifest(self.model_size, model_dir)
# RequestException=Exception under the test mocks; do not catch
# Timeout separately (it is not a real exception type there).
except requests.exceptions.RequestException as e:
@@ -2417,6 +2419,17 @@ class SpeechRecognitionManager:
if self._download_progress_callback:
self._download_progress_callback(1.0, 0, "Complete!")
@staticmethod
def _validate_parakeet_release_manifest(model_size: str, model_dir: str) -> None:
"""Discard failed publisher metadata so retries can reuse verified weights."""
try:
parakeet.validate_release_manifest(model_size, model_dir)
except ChecksumError:
filename = parakeet.PARAKEET_MODEL_INFO[model_size].get("manifest")
if filename:
os.remove(os.path.join(model_dir, filename))
raise
@staticmethod
def _parakeet_model_is_verified(model_size: str, model_dir: str) -> bool:
"""Hash each bundle file against its pin. Delete only a digest/size mismatch.
@@ -2425,7 +2438,7 @@ class SpeechRecognitionManager:
so the caller does not hand the files to sherpa-onnx.
"""
verified = True
for filename in parakeet.MODEL_FILES:
for filename in parakeet.model_files(model_size):
path = os.path.join(model_dir, filename)
key = parakeet.manifest_key(model_size, filename)
try:
@@ -2441,6 +2454,12 @@ class SpeechRecognitionManager:
logger.error("Could not remove %s: %s", path, remove_error)
return False
logger.info("Removed the unverified model file; it will be downloaded again")
if verified:
try:
SpeechRecognitionManager._validate_parakeet_release_manifest(model_size, model_dir)
except (ChecksumError, OSError, ValueError) as error:
logger.error("Parakeet release manifest verification failed: %s", error)
return False
return verified
def _init_parakeet(self) -> None:
@@ -2509,7 +2528,7 @@ class SpeechRecognitionManager:
logger.error("Please install it with 'pip install sherpa-onnx'")
self.state = RecognitionState.ERROR
raise
except (FileNotFoundError, RuntimeError, OSError) as e:
except (ChecksumError, FileNotFoundError, RuntimeError, OSError) as e:
logger.error(f"Failed to initialize Parakeet engine: {e}", exc_info=True)
self.state = RecognitionState.ERROR
raise
+8
View File
@@ -89,6 +89,14 @@ vosk-model-small-uk-v3-small.zip sha256 ee05b6a8c796c53c61cd1c
vosk-model-small-vn-0.4.zip sha256 efe5c8494212110471a79befc48c79da679e5b1fc52a4ffb500222ff86d622e5 33656337
vosk-model-uk-v3.zip sha256 b1a0dbb9a19bfb6cdbad5bb4d43b71281ab57ad4a8c2a372656c83b994b72b1f 371048965
vosk-model-vn-0.4.zip sha256 a8ce6ee52e369b8f23f56e87e037db10322e6d94779b08d5e1ab21cb4b737380 74423562
parakeet-orukeet-v0.1.0-manifest.json sha256 7e80f93f0e9b923c392424b0f85d28a717feee0a4d2a6aa9bfa723693868e727 1867
parakeet-orukeet-v0.1.0-encoder.int8.onnx sha256 7b55f2a504a20a8e462899f5befd45f4a1784948d76ed0127902d9cf39405487 653182378
parakeet-orukeet-v0.1.0-decoder.int8.onnx sha256 c185c2afb4c77c94bb1314807ecb3dc1623057a3dc540b83e10301af9bf4cfca 11845332
parakeet-orukeet-v0.1.0-joiner.int8.onnx sha256 1a7e90abf7172d926dd7e2edac2a5d5035c24dfb641a15e131d57b6a5f63cdd3 6355335
parakeet-orukeet-v0.1.0-tokens.txt sha256 d58544679ea4bc6ac563d1f545eb7d474bd6cfa467f0a6e2c1dc1c7d37e3c35d 93939
parakeet-orukeet-v0.1.0-bpe.vocab sha256 41d5e71b3591642eff088151efd7acd4e750124cc0054c8ba9fa3245187a4804 117408
parakeet-orukeet-v0.1.0-LICENSE-WEIGHTS sha256 23ee78c8bae49cf08ea2f0c84945c66b987ebe4520881fb51b3dad4fb43d07c2 20137
parakeet-orukeet-v0.1.0-NOTICE.md sha256 440361d963edd9621e744f251332b47f2c4de2e2594ecfe42b215e3f6223fa44 5271
parakeet-v3-european-encoder.int8.onnx sha256 acfc2b4456377e15d04f0243af540b7fe7c992f8d898d751cf134c3a55fd2247 652184281
parakeet-v3-european-decoder.int8.onnx sha256 179e50c43d1a9de79c8a24149a2f9bac6eb5981823f2a2ed88d655b24248db4e 11845275
parakeet-v3-european-joiner.int8.onnx sha256 3164c13fc2821009440d20fcb5fdc78bff28b4db2f8d0f0b329101719c0948b3 6355277
+56 -3
View File
@@ -5,6 +5,7 @@ This module provides model metadata and download URLs for the Parakeet
engine, which runs NVIDIA NeMo ASR models through sherpa-onnx.
"""
import json
import logging
import os
import shutil
@@ -22,6 +23,24 @@ logger = logging.getLogger(__name__)
_HF_BASE_URL = "https://huggingface.co"
PARAKEET_MODEL_INFO: Dict[str, Dict[str, Any]] = {
"orukeet-v0.1.0": {
"repo": "oruk/orukeet",
"revision": "eac739d754bb171287930e6e63386f5b88f8179e",
"subdir": "onnx/sherpa-v0.1.0-int8",
"manifest": "manifest.json",
"files": [
"manifest.json",
"encoder.int8.onnx",
"decoder.int8.onnx",
"joiner.int8.onnx",
"tokens.txt",
"bpe.vocab",
"LICENSE-WEIGHTS",
"NOTICE.md",
],
"size_mb": 641,
"desc": "Orukeet v0.1.0 (int8), 25 European languages; CC BY-SA 4.0",
},
"v3-european": {
"repo": "csukuangfj/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8",
"revision": "2bda32ec70b097a55adaa07d9a7173915b43cc78",
@@ -36,7 +55,7 @@ PARAKEET_MODEL_INFO: Dict[str, Dict[str, Any]] = {
},
}
MODEL_SIZES = ["v3-european", "v2-english"]
MODEL_SIZES = ["v3-european", "v2-english", "orukeet-v0.1.0"]
# Model offered when the engine has nothing downloaded yet.
RECOMMENDED_MODEL = "v3-european"
@@ -73,7 +92,7 @@ def is_model_downloaded(model_name: str) -> bool:
True if model exists, False otherwise
"""
model_path = get_model_path(model_name)
return all(os.path.exists(os.path.join(model_path, f)) for f in MODEL_FILES)
return all(os.path.exists(os.path.join(model_path, f)) for f in model_files(model_name))
def list_downloaded_models() -> list[str]:
@@ -134,7 +153,41 @@ def get_model_file_url(model_name: str, filename: str) -> str:
if not model_info:
raise ValueError(f"Unknown Parakeet model: {model_name}")
relative = "/".join(part for part in (model_info.get("subdir", ""), filename) if part)
return (
f"{_HF_BASE_URL}/{model_info['repo']}/resolve/{model_info['revision']}"
f"/{filename}?download=true"
f"/{relative}?download=true"
)
def model_files(model_name: str) -> list[str]:
"""Return this model's runtime files, release manifest, and license notices."""
return list(PARAKEET_MODEL_INFO[model_name].get("files", MODEL_FILES))
def validate_release_manifest(model_name: str, model_dir: str) -> None:
"""Check the downloaded publisher manifest against the app's pinned files."""
from .model_checksums import ChecksumError, expected_for
filename = PARAKEET_MODEL_INFO[model_name].get("manifest")
if not filename:
return
try:
with open(os.path.join(model_dir, filename), encoding="utf-8") as source:
manifest = json.load(source)
published = {record["path"]: record for record in manifest["files"]}
for name in model_files(model_name):
if name == filename:
continue
expected = expected_for(manifest_key(model_name, name))
record = published.get(name)
if (
expected is None
or record is None
or expected.algo != "sha256"
or expected.digest != record["sha256"]
or expected.size != record["bytes"]
):
raise ChecksumError(f"Release manifest disagrees with pinned Parakeet file: {name}")
except (KeyError, TypeError, ValueError) as error:
raise ChecksumError(f"Malformed Parakeet release manifest: {filename}") from error
+2 -2
View File
@@ -34,7 +34,7 @@ from vocalinux.utils.model_checksums import (
whispercpp_revision,
write_verification_stamp,
)
from vocalinux.utils.parakeet_model_info import MODEL_FILES, PARAKEET_MODEL_INFO, manifest_key
from vocalinux.utils.parakeet_model_info import PARAKEET_MODEL_INFO, manifest_key, model_files
from vocalinux.utils.vosk_model_info import VOSK_MODEL_INFO
from vocalinux.utils.whispercpp_model_info import (
WHISPERCPP_MODEL_INFO,
@@ -84,7 +84,7 @@ class TestManifestCoverage(unittest.TestCase):
missing = [
manifest_key(name, filename)
for name in PARAKEET_MODEL_INFO
for filename in MODEL_FILES
for filename in model_files(name)
if manifest_key(name, filename) not in pinned
]
self.assertEqual(
+63
View File
@@ -0,0 +1,63 @@
"""Optional Orukeet catalog and publisher-manifest verification."""
import json
from pathlib import Path
import pytest
from vocalinux.utils import parakeet_model_info as models
from vocalinux.utils.model_checksums import ChecksumError, expected_for
MODEL = "orukeet-v0.1.0"
def test_orukeet_is_optional_and_keeps_license_files() -> None:
assert models.RECOMMENDED_MODEL == "v3-european"
assert models.MODEL_SIZES[:2] == ["v3-european", "v2-english"]
assert models.model_files("v3-european") == models.MODEL_FILES
assert models.model_files(MODEL)[0] == "manifest.json"
assert {"LICENSE-WEIGHTS", "NOTICE.md"} <= set(models.model_files(MODEL))
assert models.get_model_file_url(MODEL, "encoder.int8.onnx") == (
"https://huggingface.co/oruk/orukeet/resolve/"
"eac739d754bb171287930e6e63386f5b88f8179e/onnx/sherpa-v0.1.0-int8/"
"encoder.int8.onnx?download=true"
)
def test_release_manifest_must_agree_with_every_pinned_file(tmp_path) -> None:
files = []
for name in models.model_files(MODEL):
if name == "manifest.json":
continue
expected = expected_for(models.manifest_key(MODEL, name))
assert expected is not None
files.append({"path": name, "sha256": expected.digest, "bytes": expected.size})
manifest_path = tmp_path / "manifest.json"
manifest_path.write_text(json.dumps({"files": files}), encoding="utf-8")
models.validate_release_manifest(MODEL, str(tmp_path))
files[0]["sha256"] = "0" * 64
manifest_path.write_text(json.dumps({"files": files}), encoding="utf-8")
with pytest.raises(ChecksumError, match="disagrees"):
models.validate_release_manifest(MODEL, str(tmp_path))
def test_missing_manifest_or_license_keeps_model_unavailable(tmp_path, monkeypatch) -> None:
monkeypatch.setattr(models, "get_model_path", lambda _: str(tmp_path))
for name in models.model_files(MODEL):
(tmp_path / name).touch()
assert models.is_model_downloaded(MODEL)
(tmp_path / "NOTICE.md").unlink()
assert not models.is_model_downloaded(MODEL)
(tmp_path / "NOTICE.md").touch()
(tmp_path / "manifest.json").unlink()
assert not models.is_model_downloaded(MODEL)
@pytest.mark.parametrize(
"content",
["{", "null", "{}", '{"files": null}', '{"files": [{}]}', '{"files": [1]}'],
)
def test_malformed_release_manifest_is_a_verification_failure(tmp_path: Path, content: str) -> None:
(tmp_path / "manifest.json").write_text(content, encoding="utf-8")
with pytest.raises(ChecksumError, match="Malformed"):
models.validate_release_manifest(MODEL, str(tmp_path))
@@ -8,9 +8,11 @@ Key focus areas:
"""
import base64
import json
import os
import sys
import time
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
@@ -31,6 +33,7 @@ from vocalinux.utils.model_checksums import VERIFICATION_STAMP_NAME, ChecksumErr
from vocalinux.utils.model_checksums import verify_model_file as verify_model_file_real
from vocalinux.utils.parakeet_model_info import MODEL_FILES as PARAKEET_MODEL_FILES
from vocalinux.utils.parakeet_model_info import manifest_key as parakeet_manifest_key
from vocalinux.utils.parakeet_model_info import model_files as parakeet_model_files
def _make_manager(engine="whisper_cpp", **kw):
@@ -804,6 +807,85 @@ class TestParakeetDownloadVerifiesExistingFiles:
assert encoder.read_bytes() == b"good-enough"
class TestParakeetReleaseManifestRecovery:
"""Invalid publisher metadata must leave a retryable, unloaded model."""
@pytest.mark.parametrize("cached", [False, True])
@pytest.mark.parametrize("invalid_manifest", ['{"files": []}', '{"files": [{}]}'])
def test_invalid_manifest_is_removed_and_retry_reuses_weights(
self,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
skip_checksum: MagicMock,
cached: bool,
invalid_manifest: str,
) -> None:
from vocalinux.utils import parakeet_model_info as parakeet
model_name = "orukeet-v0.1.0"
model_dir = tmp_path / model_name
model_dir.mkdir()
names = parakeet_model_files(model_name)
manifest_path = model_dir / "manifest.json"
records = []
for name in names[1:]:
expected = expected_for(parakeet_manifest_key(model_name, name))
assert expected is not None
records.append({"path": name, "sha256": expected.digest, "bytes": expected.size})
if cached:
(model_dir / name).write_bytes(b"verified model file")
if cached:
manifest_path.write_text(invalid_manifest, encoding="utf-8")
manager = _make_manager(engine="parakeet")
manager.model_size = model_name
manager._defer_download = cached
progress = MagicMock()
manager._download_progress_callback = progress
streamed = []
content = invalid_manifest
def stream(url: str, destination: str) -> None:
streamed.append(os.path.basename(destination))
with open(destination, "wb") as target:
target.write(
content.encode()
if destination.endswith("manifest.json.tmp")
else b"verified model file"
)
mock_sherpa = MagicMock()
mock_requests = MagicMock()
mock_requests.exceptions.RequestException = FakeRequestError
monkeypatch.setitem(sys.modules, "sherpa_onnx", mock_sherpa)
monkeypatch.setitem(sys.modules, "requests", mock_requests)
monkeypatch.setattr(parakeet, "get_model_path", lambda _: str(model_dir))
monkeypatch.setattr(manager, "_stream_model_download", stream)
if cached:
manager._init_parakeet()
else:
with pytest.raises(ChecksumError):
manager._init_parakeet()
assert manager.state == RecognitionState.ERROR
assert not manager._model_initialized
mock_sherpa.OfflineRecognizer.from_transducer.assert_not_called()
assert not manifest_path.exists()
assert not parakeet.is_model_downloaded(model_name)
assert manager._download_progress_callback is progress
assert all(call.args[2] != "Complete!" for call in progress.call_args_list)
assert all((model_dir / name).read_bytes() == b"verified model file" for name in names[1:])
content = json.dumps({"files": records})
manager._defer_download = False
streamed.clear()
manager._init_parakeet()
assert manager._model_initialized
assert parakeet.is_model_downloaded(model_name)
assert streamed == ["manifest.json.tmp"]
assert progress.call_args.args == (1.0, 0, "Complete!")
class TestFasterWhisperRejectsAnUnverifiedModelOnDisk:
"""Wiring test: the hash has to happen where the model is picked up."""