feat(speech_recognition): add faster-whisper engine backend (#543)

* feat(speech_recognition): add EngineType enum and Engine Protocol

Introduce a typed EngineType enum and an Engine Protocol in common_types.py,
plus an engines/ registry so new speech recognition backends can be plugged in
without scattering string comparisons across the manager.

* feat(speech_recognition): add faster-whisper engine backend

Add a faster-whisper engine implementation using CTranslate2/INT8 for fast
CPU inference. Includes model metadata utility, wiring through the recognition
manager, CLI, settings UI, config, optional dependency, and unit tests. Also
updates the suspend-handler test fixture for the new engine attribute.

* style(ui): format faster-whisper imports for isort 8

* test(speech_recognition): expand faster-whisper coverage for codecov

* test(speech_recognition): fix failing faster-whisper coverage tests

- Import missing get_compute_type helper

- Add tests for English-only detection, cache exceptions, and import-error registry path

- Clear _has_torch_cuda lru_cache around exception test to avoid cached True

- Replace broad builtins.__import__ patch with a fake engine module for registry test

- Restore registry module state after test

* test(speech_recognition): add coverage for faster-whisper edge cases

Cover previously missing lines in:

- faster_whisper_model_info.py: HF cache path/none, hf_hub lookup, fallback traversal, torch CUDA true, psutil exception

- faster_whisper_engine.py: model None guard after is_ready()

- engine registry import-error path

* feat(install): add faster_whisper engine option to install.sh

- Add faster_whisper to --engine help text and examples

- Add faster-whisper as interactive option 5 with description

- Install faster-whisper package and verify import at install time

- Add fallback to whisper.cpp if faster-whisper installation fails

- Add smoke test for faster-whisper import

- Update welcome message engine display name

- Note: faster-whisper models download automatically on first use

* refactor(faster_whisper): apply ponytail review cleanups

- Inline FASTER_WHISPER_MODEL_INFO dict literal; drop unused AVAILABLE_MODELS/MODEL_SIZES

- Reuse _has_torch_cuda() in FasterWhisperEngine.device property

- Remove redundant model None guard after is_ready() verification

- Shrink faster-whisper default config in install.sh to engine + model_size

- Update device tests to manage _has_torch_cuda lru_cache

- Remove test for deleted dead-code guard

* fix(faster_whisper): integrate optional engine with current main

Keep whisper_cpp as default and Parakeet as an extra. Put engine selection
in the Advanced island, install faster-whisper via the optional extra, and
refuse unpinned Hugging Face downloads until model_checksums.txt pins them.

* fix(faster_whisper): download verified models and refresh uv.lock

Settings already opened a download dialog for uncached faster-whisper, but
init only raised ChecksumError. Wire a Parakeet-style downloader that stores
bundles under models_dir, verifies each file against model_checksums.txt, and
loads WhisperModel from that path. Regenerate uv.lock for the optional
faster-whisper extra, pin digests for all eleven Systran models, and add the
missing -> None return annotations.

* fix(faster_whisper): clear engine on unload and fix auto-pause fixture

Auto-pause tests stub __init__, so they never set _faster_whisper_engine.
reinitialize_after_resume then AttributeErrors once Test Python runs.
Mirror the suspend-handler fixture, and release the faster-whisper engine
inside unload_model the same way resume already does.

* fix(faster_whisper): restore previous engine after a failed switch

reconfigure() selected the new engine and released the live model before
init. A failed faster-whisper download then left the manager in ERROR on
the new backend while Settings reverted the pickers to the saved engine,
so dictation stayed dead until restart. Reload the previous engine from
disk (no download) before re-raising so the UI and live manager agree.

* fix(speech_recognition): restore all live settings after failed switch

Failed reconfigure already put the previous engine back, but VAD, device,
remote API, and whisper.cpp knobs stayed on the attempted values while
config on disk did not. Snapshot every field reconfigure writes and restore
the lot before reloading the old backend. Also bind the real
_init_selected_engine onto the MagicMock in the force_reinit test.

* fix(speech_recognition): restore every live field after failed reconfigure

A failed engine switch already reloaded the previous backend, but Greptile
could still miss that VAD, audio device, remote API, and whisper.cpp knobs
were part of the snapshot. Keep one explicit attr list for snapshot/restore,
including the derived voice-commands flag, so live state matches persisted
config after a failed Settings apply.

* chore(speech_recognition): sign tip after rollback follow-up
This commit is contained in:
Jatin K Malik
2026-09-07 20:47:56 -07:00
committed by GitHub
parent 7664266823
commit cdfe541c25
25 changed files with 2313 additions and 148 deletions
+67 -6
View File
@@ -365,7 +365,7 @@ while [[ $# -gt 0 ]]; do
echo "Options:"
echo " --interactive, -i Force interactive mode (default)"
echo " --auto Non-interactive automatic installation"
echo " --engine=NAME Speech engine: whisper_cpp (default), whisper, vosk, parakeet, remote_api"
echo " --engine=NAME Speech engine: whisper_cpp (default), whisper, vosk, parakeet, faster_whisper, remote_api"
echo " --dev Install in development mode with all dev dependencies"
echo " --test Run tests after installation"
echo " --venv-dir=PATH Specify custom virtual environment directory"
@@ -381,6 +381,7 @@ while [[ $# -gt 0 ]]; do
echo " $0 # Interactive mode (recommended)"
echo " $0 --auto # Auto-install with whisper.cpp"
echo " $0 --auto --engine=vosk # Auto-install VOSK only"
echo " $0 --auto --engine=faster_whisper # Auto-install faster-whisper"
echo " $0 --dev --test # Dev mode with tests"
echo ""
echo "During installation a full transcript is saved to"
@@ -1251,7 +1252,15 @@ EOF
echo " └─────────────────────────────────────────────────────────────┘"
echo ""
echo " ┌───────────────────────────────────────────────────────────────┐"
echo " │ 4. REMOTE API (ADVANCED) │"
echo " │ 4. FASTER-WHISPER │"
echo " │ • Optional CTranslate2 Whisper backend │"
echo " │ • Fast on CPU with INT8 quantization │"
echo " │ • Best performance on NVIDIA GPUs (CUDA) │"
echo " │ • Hugging Face models stay checksum-gated (not auto-dl) │"
echo " └───────────────────────────────────────────────────────────────┘"
echo ""
echo " ┌───────────────────────────────────────────────────────────────┐"
echo " │ 5. REMOTE API (ADVANCED) │"
echo " │ • Offload processing to a GPU server on your network │"
echo " │ • Ideal for laptops without GPU │"
echo " │ • Supports whisper.cpp server & OpenAI-compatible APIs │"
@@ -1277,7 +1286,7 @@ EOF
esac
echo ""
read -p "Choose engine [1-4] (default: $DEFAULT_CHOICE): " ENGINE_CHOICE
read -p "Choose engine [1-5] (default: $DEFAULT_CHOICE): " ENGINE_CHOICE
ENGINE_CHOICE=${ENGINE_CHOICE:-$DEFAULT_CHOICE}
case "$ENGINE_CHOICE" in
@@ -1294,6 +1303,10 @@ EOF
ENGINE_DISPLAY="VOSK (Lightweight)"
;;
4)
SELECTED_ENGINE="faster_whisper"
ENGINE_DISPLAY="Faster-Whisper"
;;
5)
SELECTED_ENGINE="remote_api"
ENGINE_DISPLAY="Remote API"
;;
@@ -1547,13 +1560,12 @@ fi
# Set default engine for auto/non-interactive mode
if [[ "$NON_INTERACTIVE" == "yes" ]] && [[ -z "$SELECTED_ENGINE" ]]; then
# Default to whisper.cpp for best performance
SELECTED_ENGINE="whisper_cpp"
print_info "Automatic mode: Installing with whisper.cpp (default engine)"
print_info "For other engines, use: --engine=whisper or --engine=vosk or --engine=remote_api"
echo ""
print_info "For other engines, use: --engine=whisper, --engine=vosk, --engine=parakeet, --engine=faster_whisper, or --engine=remote_api"
fi
# Function to check if a command exists
command_exists() {
command -v "$1" >/dev/null 2>&1
@@ -2914,6 +2926,7 @@ engine_import_module() {
whisper) echo "whisper" ;;
whisper_cpp) echo "pywhispercpp.model" ;;
parakeet) echo "sherpa_onnx" ;;
faster_whisper) echo "faster_whisper" ;;
*) echo "" ;;
esac
}
@@ -2924,6 +2937,7 @@ engine_pip_name() {
whisper) echo "openai-whisper" ;;
whisper_cpp) echo "pywhispercpp" ;;
parakeet) echo "sherpa-onnx" ;;
faster_whisper) echo "faster-whisper" ;;
*) echo "" ;;
esac
}
@@ -3611,6 +3625,35 @@ REMOTE_CONFIG
print_warning "No server URL configured. You can set it later in Settings."
fi
;;
faster_whisper)
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
mkdir -p "$CONFIG_DIR"
if [ ! -f "$CONFIG_DIR/config.json" ]; then
cat > "$CONFIG_DIR/config.json" << 'FASTER_WHISPER_CONFIG'
{
"shortcuts": {
"toggle_recognition": "right_alt+right_alt",
"mode": "push_to_talk"
}
}
FASTER_WHISPER_CONFIG
fi
set_configured_engine "$CONFIG_DIR/config.json" faster_whisper ||
print_warning "Could not point $CONFIG_DIR/config.json at the faster_whisper engine."
else
print_error "Failed to install the faster-whisper engine"
print_error "Falling back to whisper.cpp (recommended engine)"
install_cpu_pywhispercpp "$PIP_LOG_FILE" || {
print_error "Failed to install whisper.cpp fallback"
return 1
}
SELECTED_ENGINE="whisper_cpp"
fi
;;
esac
fi
@@ -4573,6 +4616,20 @@ verify_installation() {
fi
fi
if [[ "$selected_engine" == "faster_whisper" ]]; then
if ! "$VENV_DIR/bin/python" -c "import faster_whisper" 2>/dev/null; then
print_error "faster-whisper package installed but cannot be imported at runtime."
print_error ""
print_error "Diagnostic steps:"
print_error " 1. Check pip installation: $VENV_DIR/bin/pip show faster-whisper"
print_error " 2. Re-run the installer with: --engine=faster_whisper"
print_error " 3. Or switch to whisper.cpp: --engine=whisper_cpp"
ISSUES=$((ISSUES + 1))
else
print_success "faster-whisper import verified successfully."
fi
fi
# Return the number of issues found
return $ISSUES
}
@@ -4627,6 +4684,10 @@ EOF
ENGINE_DISPLAY_NAME="Parakeet"
BACKEND_INFO="CPU"
;;
faster_whisper)
ENGINE_DISPLAY_NAME="Faster-Whisper"
BACKEND_INFO="PyTorch/CTranslate2"
;;
remote_api)
ENGINE_DISPLAY_NAME="Remote API"
BACKEND_INFO="Network (offloaded to remote server)"
+2 -1
View File
@@ -159,7 +159,8 @@ lock-check:
# `lfs` metadata and cost no bandwidth; VOSK is pinned by the bytes we fetch, so
# every zip not already in the manifest is downloaded. A first run, or any run
# with --refresh, pulls ~21.9GB and takes ~30 minutes.
# Run after adding a model to vosk_model_info.py or whispercpp_model_info.py.
# Run after adding a model to vosk_model_info.py, whispercpp_model_info.py,
# parakeet_model_info.py or faster_whisper_model_info.py.
model-checksums: _tooling
uv run --no-sync python scripts/generate-model-checksums.py
+4
View File
@@ -60,6 +60,9 @@ whisper = [
parakeet = [
"sherpa-onnx>=1.12.0",
]
faster_whisper = [
"faster-whisper>=1.0.0",
]
vad = [
"onnxruntime>=1.16.0",
]
@@ -114,6 +117,7 @@ packages = [
"vocalinux.resources.sounds",
"vocalinux.speech_recognition",
"vocalinux.speech_recognition.data",
"vocalinux.speech_recognition.engines",
"vocalinux.text_injection",
"vocalinux.ui",
"vocalinux.ui.keyboard_backends",
+56 -6
View File
@@ -26,8 +26,9 @@ costs:
pulls every VOSK zip: ~21.9GB, roughly half an hour on a fast connection.
The model names come from the package itself, so a model added to
vosk_model_info.py or whispercpp_model_info.py is picked up on the next run and
a model that upstream never published fails the generation loudly.
vosk_model_info.py, whispercpp_model_info.py, parakeet_model_info.py or
faster_whisper_model_info.py is picked up on the next run and a model that
upstream never published fails the generation loudly.
Usage: just model-checksums (or: python scripts/generate-model-checksums.py)
"""
@@ -162,6 +163,46 @@ def _parakeet_entries() -> list[Entry]:
return entries
def _faster_whisper_entries() -> list[Entry]:
"""Digests for every file of every faster-whisper bundle, at its pinned revision.
``model.bin`` is LFS (Hugging Face publishes sha256 on these repos); config,
tokenizer and vocabulary files are plain blobs hashed from the bytes this
script downloads, the same way Parakeet's tokens.txt is.
"""
sys.path.insert(0, str(SRC))
from vocalinux.utils.faster_whisper_model_info import (
FASTER_WHISPER_MODEL_INFO,
manifest_key,
model_files,
)
entries = []
for model_name, info in FASTER_WHISPER_MODEL_INFO.items():
repo, revision = info["repo"], info["revision"]
payload = _fetch_json(
f"https://huggingface.co/api/models/{repo}/revision/{revision}?blobs=true"
)
published = {}
for sibling in payload.get("siblings", []):
lfs = sibling.get("lfs") or {}
digest = lfs.get("sha256") or lfs.get("oid")
if digest:
published[sibling["rfilename"]] = (digest, int(lfs.get("size", 0)))
for filename in model_files(model_name):
key = manifest_key(model_name, filename)
if filename in published:
digest, size = published[filename]
else:
url = f"https://huggingface.co/{repo}/resolve/{revision}/{filename}?download=true"
print(f" {key} (not LFS, hashing bytes)...", flush=True)
digest, size = _download_and_hash(url, "")
entries.append(Entry(key, "sha256", digest, size))
return entries
def _whispercpp_entries(wanted: Iterable[str]) -> tuple[list[Entry], str]:
payload = _fetch_json(HF_API)
revision = payload.get("sha")
@@ -232,7 +273,8 @@ def _render(entries: list[Entry], revision: str) -> str:
"#",
"# Generated by scripts/generate-model-checksums.py -- do not edit by hand.",
"# Regenerate with `just model-checksums` after adding a model to",
"# vosk_model_info.py, whispercpp_model_info.py or parakeet_model_info.py.",
"# vosk_model_info.py, whispercpp_model_info.py, parakeet_model_info.py",
"# or faster_whisper_model_info.py.",
"#",
"# Read by src/vocalinux/utils/model_checksums.py and by install.sh; keep the",
"# format (filename, algorithm, digest, size in bytes) parseable by awk.",
@@ -247,8 +289,9 @@ def _render(entries: list[Entry], revision: str) -> str:
"# VOSK digests are the sha256 of bytes this script downloaded, not the md5",
"# Alphacephei publishes; that md5 is only checked against them at that point.",
"#",
"# Parakeet bundles are pinned per model in parakeet_model_info.py, so their",
"# keys carry the model name: several bundles ship the same file names.",
"# Parakeet and faster-whisper bundles are pinned per model in their",
"# model_info modules, so their keys carry the model name: several",
"# bundles ship the same file names.",
"",
]
for entry in entries:
@@ -279,8 +322,15 @@ def main() -> int:
parakeet_entries = _parakeet_entries()
print(f" Parakeet: {len(parakeet_entries)} sha256 digests")
faster_whisper_entries = _faster_whisper_entries()
print(f" faster-whisper: {len(faster_whisper_entries)} sha256 digests")
MANIFEST.write_text(
_render(whispercpp_entries + vosk_entries + parakeet_entries, revision), encoding="utf-8"
_render(
whispercpp_entries + vosk_entries + parakeet_entries + faster_whisper_entries,
revision,
),
encoding="utf-8",
)
print(f"Wrote {MANIFEST.relative_to(REPO_ROOT)}")
return 0
+35
View File
@@ -16,6 +16,41 @@ class RecognitionState(Enum):
ERROR = auto()
class EngineType(Enum):
"""Supported speech recognition engine identifiers."""
VOSK = "vosk"
WHISPER = "whisper"
WHISPER_CPP = "whisper_cpp"
PARAKEET = "parakeet"
FASTER_WHISPER = "faster_whisper"
REMOTE_API = "remote_api"
class Engine(Protocol):
"""Protocol for a speech recognition engine backend.
New engines should implement this minimal contract so they can be registered
in ``speech_recognition.engines`` and used by the manager.
"""
def init(self) -> None:
"""Initialize the engine and load any required models."""
...
def transcribe(self, audio_buffer: list[bytes]) -> str:
"""Transcribe the captured audio and return the recognized text."""
...
def is_ready(self) -> bool:
"""Return True if the engine is initialized and ready to transcribe."""
...
def cleanup(self) -> None:
"""Release any engine resources."""
...
class SpeechRecognitionManagerProtocol(Protocol):
"""Protocol defining the interface for SpeechRecognitionManager."""
+1 -1
View File
@@ -82,7 +82,7 @@ def parse_arguments():
parser.add_argument(
"--engine",
type=str,
choices=["vosk", "whisper", "whisper_cpp", "parakeet", "remote_api"],
choices=["vosk", "whisper", "whisper_cpp", "parakeet", "faster_whisper", "remote_api"],
help="Speech recognition engine to use (whisper_cpp recommended for best performance)",
)
parser.add_argument("--wayland", action="store_true", help="Force Wayland compatibility mode")
@@ -0,0 +1,21 @@
"""Speech recognition engine registry for Vocalinux.
New engine implementations are registered here so the recognition manager can
discover them without hard-coding every backend.
"""
from typing import Type
from ...common_types import Engine, EngineType
ENGINES: dict[EngineType, Type[Engine]] = {}
# Register faster-whisper when available.
try:
from .faster_whisper_engine import FasterWhisperEngine
ENGINES[EngineType.FASTER_WHISPER] = FasterWhisperEngine
except Exception:
pass
__all__ = ["ENGINES"]
@@ -0,0 +1,174 @@
"""Faster-Whisper speech recognition engine for Vocalinux.
This module wraps `faster_whisper.WhisperModel` behind the Engine Protocol. It
reuses the same audio preprocessing used by the OpenAI Whisper backend and is
designed to be significantly faster on CPU thanks to CTranslate2 and INT8
quantization.
"""
import logging
from typing import Iterable, Optional, Protocol
import numpy as np
from ...utils.faster_whisper_model_info import (
FASTER_WHISPER_MODEL_INFO,
_has_torch_cuda,
get_compute_type,
get_model_path,
get_recommended_model,
)
logger = logging.getLogger(__name__)
class _Segment(Protocol):
"""Minimal Protocol for a faster-whisper transcription segment."""
text: str
class _WhisperModel(Protocol):
"""Minimal Protocol for faster-whisper.WhisperModel API surface used here."""
def transcribe(
self,
audio: np.ndarray,
language: Optional[str],
task: str,
beam_size: int,
best_of: int,
condition_on_previous_text: bool,
) -> tuple[Iterable[_Segment], object]:
"""Transcribe audio and return segments with metadata."""
...
class FasterWhisperEngine:
"""faster-whisper backend implementing the Engine Protocol."""
def __init__(
self,
model_size: Optional[str] = None,
device: Optional[str] = None,
language: str = "auto",
) -> None:
"""Create a faster-whisper engine instance.
Args:
model_size: faster-whisper model name (e.g. "tiny"). Defaults to the
recommended model for the current system.
device: "cpu" or "cuda". Auto-detected when None.
language: Language code ("auto" for auto-detect).
"""
self.model_size = model_size or get_recommended_model()[0]
self.language = language
self._device = device
self._model: Optional[_WhisperModel] = None
self._model_initialized = False
@property
def device(self) -> str:
"""Return the compute device, detecting CUDA if not explicitly set."""
if self._device is not None:
return self._device
return "cuda" if _has_torch_cuda() else "cpu"
def init(self) -> None:
"""Load the faster-whisper model."""
if self._model_initialized:
return
try:
from faster_whisper import WhisperModel
except ImportError as e:
logger.error(f"Failed to import faster-whisper: {e}")
raise
if self.model_size not in FASTER_WHISPER_MODEL_INFO:
logger.warning(
f"Model size '{self.model_size}' not valid for faster-whisper. "
f"Valid options: {list(FASTER_WHISPER_MODEL_INFO.keys())}. Using 'tiny'."
)
self.model_size = "tiny"
device = self.device
compute_type = get_compute_type(device)
logger.info(
f"Loading faster-whisper '{self.model_size}' model on {device} "
f"with compute_type={compute_type}"
)
# Load the checksum-gated snapshot from models_dir. local_files_only
# keeps faster-whisper from fetching an unpinned Hugging Face revision.
model_dir = get_model_path(self.model_size)
self._model = WhisperModel(
model_dir,
device=device,
compute_type=compute_type,
local_files_only=True,
)
self._model_initialized = True
logger.info("faster-whisper engine initialized successfully.")
def is_ready(self) -> bool:
"""Return True if the model has been loaded successfully."""
return self._model_initialized and self._model is not None
def cleanup(self) -> None:
"""Release the loaded model."""
self._model = None
self._model_initialized = False
def _normalize_language(self) -> Optional[str]:
"""Map Vocalinux language codes to faster-whisper language codes."""
if self.language == "auto":
return None
if self.language == "en-us" or self.language == "en-in":
return "en"
return self.language
def transcribe(self, audio_buffer: list[bytes]) -> str:
"""Transcribe the provided audio buffer.
Args:
audio_buffer: List of audio data chunks (16-bit PCM at 16kHz).
Returns:
Recognized text.
"""
if not self.is_ready():
logger.warning("faster-whisper engine is not initialized")
return ""
model = self._model
if model is None or not audio_buffer:
return ""
try:
audio_data = np.frombuffer(b"".join(audio_buffer), dtype=np.int16)
audio_float = audio_data.astype(np.float32) / 32768.0
segments, _info = model.transcribe(
audio_float,
language=self._normalize_language(),
task="transcribe",
beam_size=5,
best_of=5,
condition_on_previous_text=False,
)
text_parts = [segment.text.strip() for segment in segments if segment.text]
text = " ".join(text_parts).strip()
if text:
logger.info(f"faster-whisper transcribed: '{text}'")
else:
logger.debug("faster-whisper returned empty transcription")
return text
except (RuntimeError, OSError, ValueError) as e:
logger.error(f"Error in faster-whisper transcription: {e}", exc_info=True)
return ""
@@ -21,6 +21,7 @@ from typing import Callable, Optional
from ..common_types import RecognitionState
from ..ui.audio_feedback import play_error_sound, play_start_sound, play_stop_sound
from ..utils import faster_whisper_model_info as faster_whisper
from ..utils import parakeet_model_info as parakeet
from ..utils.host_process import host_env
from ..utils.model_checksums import (
@@ -1062,6 +1063,8 @@ class SpeechRecognitionManager:
self.remote_api_model = kwargs.get("remote_api_model", "whisper-1")
self._http_session = None
self._faster_whisper_engine = None
# Audio diagnostics tracking
self._last_audio_level = 0.0
self._audio_level_callbacks: list[Callable[[float], None]] = []
@@ -1101,6 +1104,8 @@ class SpeechRecognitionManager:
self._init_whispercpp()
elif engine == "parakeet":
self._init_parakeet()
elif engine == "faster_whisper":
self._init_faster_whisper()
elif engine == "remote_api":
self._init_remote_api()
else:
@@ -1112,6 +1117,62 @@ class SpeechRecognitionManager:
return self.engine == "vosk"
return bool(self._voice_commands_preference)
def _init_selected_engine(self) -> None:
"""Initialize whichever engine ``self.engine`` currently names."""
if self.engine == "vosk":
self._init_vosk()
elif self.engine == "whisper":
self._init_whisper()
elif self.engine == "whisper_cpp":
self._init_whispercpp()
elif self.engine == "parakeet":
self._init_parakeet()
elif self.engine == "faster_whisper":
self._init_faster_whisper()
elif self.engine == "remote_api":
self._init_remote_api()
else:
raise ValueError(f"Unsupported speech recognition engine: {self.engine}")
# Every live field ``reconfigure()`` can write. A failed engine switch
# restores this set so dictation matches the persisted config, not the
# attempted-but-unsaved Settings values.
_RECONFIGURE_STATE_ATTRS: tuple[str, ...] = (
"engine",
"model_size",
"language",
"vad_sensitivity",
"silence_timeout",
"audio_device_index",
"audio_device_name",
"_voice_commands_preference",
"_voice_commands_enabled",
"stop_sound_guard_ms",
"whispercpp_no_timestamps",
"whispercpp_no_context",
"whispercpp_initial_prompt",
"whispercpp_temperature",
"whispercpp_temperature_inc",
"whispercpp_entropy_thold",
"whispercpp_logprob_thold",
"whispercpp_no_speech_thold",
"whispercpp_n_threads",
"whispercpp_gpu_device",
"remote_api_url",
"remote_api_key",
"remote_api_endpoint",
"remote_api_model",
)
def _snapshot_reconfigure_state(self) -> dict[str, object]:
"""Copy every live setting ``reconfigure()`` is about to mutate."""
return {name: getattr(self, name) for name in self._RECONFIGURE_STATE_ATTRS}
def _restore_reconfigure_state(self, previous: dict[str, object]) -> None:
"""Write a ``_snapshot_reconfigure_state`` result back onto the manager."""
for name, value in previous.items():
setattr(self, name, value)
def _init_vosk(self):
"""Initialize the VOSK speech recognition engine."""
# VOSK doesn't support auto-detect, so fall back to en-us for "auto"
@@ -1301,6 +1362,212 @@ class SpeechRecognitionManager:
logger.error(f"Error in Whisper transcription: {e}", exc_info=True)
return ""
def _download_faster_whisper_model(self) -> None:
"""Download the faster-whisper model files with progress tracking."""
import requests
self._download_cancelled = False
model_dir = faster_whisper.get_model_path(self.model_size)
os.makedirs(model_dir, exist_ok=True)
logger.info(f"Downloading faster-whisper '{self.model_size}' model to {model_dir}")
bundle_files = faster_whisper.model_files(self.model_size)
# _stream_model_download reports 0..1 per file, so remap each file into
# its own slice of the bundle: the bar advances once across the whole
# download instead of restarting for each file.
outer_callback = self._download_progress_callback
total_files = len(bundle_files)
def file_progress(index: int, name: str) -> Callable[[float, float, str], None]:
def report(fraction: float, speed: float, status: str) -> None:
if outer_callback:
outer_callback(
(index + fraction) / total_files,
speed,
f"{name} ({index + 1}/{total_files}) - {status}",
)
return report
temp_file = None
try:
for index, filename in enumerate(bundle_files):
dest_path = os.path.join(model_dir, filename)
key = faster_whisper.manifest_key(self.model_size, filename)
# Existence is not enough: a leftover or copied-in file must
# still match its pin. Verify, and only skip the download when
# the digest is good.
if os.path.exists(dest_path):
try:
verify_model_file(dest_path, key)
continue
except ChecksumError as error:
logger.error(
"Existing faster-whisper model file at %s is not trustworthy: %s",
dest_path,
error,
)
if expected_for(key) is None:
raise
try:
os.remove(dest_path)
except OSError as remove_error:
logger.error("Could not remove %s: %s", dest_path, remove_error)
raise
logger.info(
"Removed the unverified model file; it will be downloaded again"
)
temp_file = dest_path + ".tmp"
url = faster_whisper.get_model_file_url(self.model_size, filename)
self._download_progress_callback = file_progress(index, filename)
self._stream_model_download(url, temp_file)
# Verify before the rename: CTranslate2 loads these through
# native code, so an unverified file must never reach a path
# is_model_downloaded() trusts.
verify_model_file(temp_file, key)
os.rename(temp_file, dest_path)
temp_file = None
# 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:
logger.error(f"Failed to download the faster-whisper model: {e}")
if temp_file and os.path.exists(temp_file):
os.remove(temp_file)
msg = str(e).lower()
if "timeout" in msg or type(e).__name__ == "Timeout":
raise RuntimeError(
"Model download timed out (Hugging Face may be slow or unavailable). "
"Check your network and try again."
) from e
raise RuntimeError(f"Failed to download faster-whisper model: {e}") from e
except (ChecksumError, OSError, RuntimeError, ValueError) as e:
logger.error(f"An error occurred during faster-whisper model download: {e}")
if temp_file and os.path.exists(temp_file):
os.remove(temp_file)
raise
finally:
self._download_progress_callback = outer_callback
logger.info("faster-whisper model downloaded successfully")
if self._download_progress_callback:
self._download_progress_callback(1.0, 0, "Complete!")
@staticmethod
def _faster_whisper_model_is_verified(model_size: str, model_dir: str) -> bool:
"""Hash each bundle file against its pin. Delete only a digest/size mismatch.
An unpinned name is refused, not deleted. If remove fails, return False
so the caller does not hand the files to CTranslate2.
"""
verified = True
for filename in faster_whisper.model_files(model_size):
path = os.path.join(model_dir, filename)
key = faster_whisper.manifest_key(model_size, filename)
try:
verify_model_file(path, key)
except ChecksumError as error:
logger.error("faster-whisper model file at %s is not trustworthy: %s", path, error)
verified = False
if expected_for(key) is None:
continue
try:
os.remove(path)
except OSError as remove_error:
logger.error("Could not remove %s: %s", path, remove_error)
return False
logger.info("Removed the unverified model file; it will be downloaded again")
return verified
def _init_faster_whisper(self) -> None:
"""Initialize the faster-whisper speech recognition engine."""
try:
from .engines.faster_whisper_engine import FasterWhisperEngine
except ImportError as e:
logger.error(f"Failed to import faster-whisper engine: {e}")
logger.error("Please install with: pip install faster-whisper")
self.state = RecognitionState.ERROR
raise
try:
valid_models = list(faster_whisper.FASTER_WHISPER_MODEL_INFO.keys())
if self.model_size not in valid_models:
logger.warning(
f"Model size '{self.model_size}' not valid for faster-whisper. "
f"Valid options: {valid_models}. Using 'tiny' instead."
)
self.model_size = "tiny"
model_dir = faster_whisper.get_model_path(self.model_size)
# A bundle that is merely present did not necessarily come through
# the download path: files could be copied in, or left over from a
# cancelled download. CTranslate2 loads these through native code,
# so hash them here; a failure demotes the bundle to "not downloaded".
if faster_whisper.is_model_downloaded(
self.model_size
) and not self._faster_whisper_model_is_verified(self.model_size, model_dir):
if faster_whisper.is_model_downloaded(self.model_size):
logger.error(
"Refusing to load unverified faster-whisper model at %s",
model_dir,
)
self._model_initialized = False
if self._defer_download:
return
raise RuntimeError(f"faster-whisper model at {model_dir} failed verification")
if not faster_whisper.is_model_downloaded(self.model_size):
if self._defer_download:
logger.info(
f"faster-whisper model '{self.model_size}' not found at {model_dir}. "
"Will download when needed."
)
self._model_initialized = False
return # Don't block startup
else:
logger.info(f"Downloading faster-whisper '{self.model_size}' model...")
self._download_faster_whisper_model()
# NOTE: do not take _model_lock here. It is a non-reentrant Lock and
# reconfigure()/reinitialize_after_resume() already hold it when they
# call this, so acquiring it would deadlock the caller.
self._faster_whisper_engine = FasterWhisperEngine(
model_size=self.model_size,
language=self.language,
)
self._faster_whisper_engine.init()
self.model = self._faster_whisper_engine._model
self._model_initialized = True
logger.info("faster-whisper engine initialized successfully.")
except (ImportError, RuntimeError, OSError, ValueError, ChecksumError) as e:
logger.error(f"Failed to initialize faster-whisper engine: {e}", exc_info=True)
self.state = RecognitionState.ERROR
raise
def _transcribe_with_faster_whisper(self, audio_buffer: list[bytes]) -> str:
"""
Transcribe audio buffer using faster-whisper.
Args:
audio_buffer: List of audio data chunks (16-bit PCM at 16kHz)
Returns:
Transcribed text
"""
try:
if not self._faster_whisper_engine or not self._faster_whisper_engine.is_ready():
logger.warning("faster-whisper engine not ready during transcription")
return ""
return self._faster_whisper_engine.transcribe(audio_buffer)
except (RuntimeError, OSError, ValueError) as e:
logger.error(f"Error in faster-whisper transcription: {e}", exc_info=True)
return ""
def _init_whispercpp(self):
"""Initialize the whisper.cpp speech recognition engine."""
try:
@@ -3255,6 +3522,9 @@ class SpeechRecognitionManager:
elif self.engine == "parakeet":
text = self._transcribe_with_parakeet(audio_buffer)
elif self.engine == "faster_whisper":
text = self._transcribe_with_faster_whisper(audio_buffer)
elif self.engine == "remote_api":
# Snapshot the HTTP session under lock to prevent race with
# reconfigure() / reinitialize_after_resume() which close/recreate
@@ -3424,6 +3694,11 @@ class SpeechRecognitionManager:
f"audio_device={audio_device_index}, audio_device_name={audio_device_name}"
)
whispercpp_attrs = tuple(
name for name in self._RECONFIGURE_STATE_ATTRS if name.startswith("whispercpp_")
)
previous = self._snapshot_reconfigure_state()
restart_needed = force_reinit
old_engine = self.engine
if engine is not None and engine != self.engine:
@@ -3477,18 +3752,7 @@ class SpeechRecognitionManager:
if "stop_sound_guard_ms" in kwargs:
self.stop_sound_guard_ms = kwargs.get("stop_sound_guard_ms", self.stop_sound_guard_ms)
for param_name in (
"whispercpp_no_timestamps",
"whispercpp_no_context",
"whispercpp_initial_prompt",
"whispercpp_temperature",
"whispercpp_temperature_inc",
"whispercpp_entropy_thold",
"whispercpp_logprob_thold",
"whispercpp_no_speech_thold",
"whispercpp_n_threads",
"whispercpp_gpu_device",
):
for param_name in whispercpp_attrs:
if param_name in kwargs:
setattr(self, param_name, kwargs[param_name])
restart_needed = True
@@ -3530,28 +3794,39 @@ class SpeechRecognitionManager:
# Release old resources explicitly if necessary (Python's GC might handle it)
self.model = None
self.recognizer = None
if self._faster_whisper_engine is not None:
self._faster_whisper_engine.cleanup()
self._faster_whisper_engine = None
if old_engine == "remote_api" and self.engine != "remote_api":
if self._http_session is not None:
self._http_session.close()
self._http_session = None
try:
if self.engine == "vosk":
self._init_vosk()
elif self.engine == "whisper":
self._init_whisper()
elif self.engine == "whisper_cpp":
self._init_whispercpp()
elif self.engine == "parakeet":
self._init_parakeet()
elif self.engine == "remote_api":
self._init_remote_api()
else:
raise ValueError(f"Unsupported engine during reconfigure: {self.engine}")
self._init_selected_engine()
logger.info("Speech engine re-initialized successfully.")
except Exception as e:
logger.error(f"Failed to re-initialize speech engine: {e}", exc_info=True)
self._update_state(RecognitionState.ERROR)
# Re-raise or handle appropriately
# Settings reverts the pickers to the saved engine on failure.
# Restore every mutated live field, then reload that engine
# so dictation is not left on the failed backend in ERROR
# with unsaved VAD/device/API knobs while the UI shows the
# previous configuration.
self._restore_reconfigure_state(previous)
self._defer_download = True
try:
self._init_selected_engine()
if self.state == RecognitionState.ERROR:
self._update_state(RecognitionState.IDLE)
logger.info(
"Restored previous speech engine %s after failed reconfigure",
self.engine,
)
except Exception:
logger.error(
"Failed to restore previous speech engine after reconfigure",
exc_info=True,
)
self._update_state(RecognitionState.ERROR)
raise
finally:
self._defer_download = old_defer
@@ -3682,6 +3957,9 @@ class SpeechRecognitionManager:
with self._model_lock:
self.model = None
self.recognizer = None
if self._faster_whisper_engine is not None:
self._faster_whisper_engine.cleanup()
self._faster_whisper_engine = None
if self._http_session is not None:
try:
self._http_session.close()
@@ -3758,6 +4036,9 @@ class SpeechRecognitionManager:
with self._model_lock:
self.model = None
self.recognizer = None
if self._faster_whisper_engine is not None:
self._faster_whisper_engine.cleanup()
self._faster_whisper_engine = None
if self._http_session is not None:
self._http_session.close()
self._http_session = None
@@ -3772,6 +4053,8 @@ class SpeechRecognitionManager:
self._init_whispercpp()
elif self.engine == "parakeet":
self._init_parakeet()
elif self.engine == "faster_whisper":
self._init_faster_whisper()
elif self.engine == "remote_api":
self._init_remote_api()
else:
+2 -1
View File
@@ -67,7 +67,7 @@ def normalize_sound_effect_tone(tone: Any) -> str:
# Default configuration
DEFAULT_CONFIG = {
"speech_recognition": { # Changed section name
"engine": "whisper_cpp", # "vosk", "whisper", or "whisper_cpp" - whisper_cpp is default for best performance
"engine": "whisper_cpp", # whisper_cpp is default; vosk/whisper/parakeet/faster_whisper/remote_api are optional
"language": "auto", # Auto-detect language (Whisper/whisper.cpp only)
"model_size": "tiny", # Current model size (for backward compatibility)
"vosk_model_size": "small", # Default model for VOSK engine
@@ -83,6 +83,7 @@ DEFAULT_CONFIG = {
# Whether the Advanced island under the simple questions is left open.
"show_advanced": False,
"parakeet_model_size": "v3-european", # Parakeet TDT 0.6B v3 (25 European languages)
"faster_whisper_model_size": "tiny", # Default model for faster-whisper engine
"vad_sensitivity": 3, # Voice Activity Detection sensitivity (1-5)
"silence_timeout": 2.0, # Seconds of silence before stopping
"stop_sound_guard_ms": 200, # Small tail trim to avoid the stop sound without clipping speech
+67 -1
View File
@@ -35,6 +35,19 @@ from gi.repository import Gdk, GLib, GObject, Gtk, Pango # noqa: E402
from ..common_types import RecognitionState # noqa: E402
from ..speech_recognition.silero_vad import is_silero_available # noqa: E402
from ..utils import parakeet_model_info as parakeet # noqa: E402
from ..utils.faster_whisper_model_info import (
FASTER_WHISPER_MODEL_INFO,
)
from ..utils.faster_whisper_model_info import delete_model as delete_faster_whisper_model
from ..utils.faster_whisper_model_info import ( # noqa: E402
get_recommended_model as get_recommended_faster_whisper_model,
)
from ..utils.faster_whisper_model_info import (
is_model_downloaded as is_faster_whisper_model_downloaded,
)
from ..utils.faster_whisper_model_info import (
list_downloaded_models as list_downloaded_faster_whisper_models,
)
from ..utils.model_choice import (
BALANCED,
PRIORITIES,
@@ -160,6 +173,13 @@ ENGINE_MODELS = {
"parakeet": [
*parakeet.MODEL_SIZES,
], # Parakeet TDT 0.6B int8 bundles
"faster_whisper": [
"tiny",
"base",
"small",
"medium",
"large-v3",
], # faster-whisper models mirror OpenAI Whisper sizes
"remote_api": [], # Remote API does not need local models
}
@@ -178,6 +198,7 @@ ENGINE_DISPLAY_NAMES = {
"whisper": "Whisper",
"whisper_cpp": "whisper.cpp",
"parakeet": "Parakeet",
"faster_whisper": "Faster Whisper",
"remote_api": "Remote API",
}
@@ -676,6 +697,7 @@ def get_available_engines():
"whisper": False,
"whisper_cpp": False,
"parakeet": False,
"faster_whisper": False,
"remote_api": False,
}
@@ -711,6 +733,13 @@ def get_available_engines():
except ImportError:
pass
try:
from faster_whisper import WhisperModel # noqa: F401
engines["faster_whisper"] = True
except ImportError:
pass
# Remote API is always available (only requires requests package)
try:
import requests # noqa: F401
@@ -1521,6 +1550,9 @@ def recommended_model_for_engine(
model_id = parakeet.RECOMMENDED_MODEL
reason = parakeet.RECOMMENDED_REASON
size_mb = parakeet.PARAKEET_MODEL_INFO.get(model_id, {}).get("size_mb", 0)
elif engine == "faster_whisper":
model_id, reason = get_recommended_faster_whisper_model()
size_mb = FASTER_WHISPER_MODEL_INFO.get(model_id, {}).get("size_mb", 0)
else:
# Remote API transcribes server-side; there is nothing to download.
return None
@@ -5308,6 +5340,8 @@ class SettingsDialog(Gtk.Dialog):
recommended_model, _ = _get_recommended_whisper_model()
elif engine == "parakeet":
recommended_model = parakeet.RECOMMENDED_MODEL
elif engine == "faster_whisper":
recommended_model, _ = get_recommended_faster_whisper_model()
else:
recommended_model, _ = _get_recommended_vosk_model()
@@ -5316,6 +5350,9 @@ class SettingsDialog(Gtk.Dialog):
if engine == "whisper" and size in WHISPER_MODEL_INFO:
info = WHISPER_MODEL_INFO[size]
is_downloaded = _is_whisper_model_downloaded(size)
elif engine == "faster_whisper" and size in FASTER_WHISPER_MODEL_INFO:
info = FASTER_WHISPER_MODEL_INFO[size]
is_downloaded = is_faster_whisper_model_downloaded(size)
elif engine == "vosk" and size in VOSK_MODEL_INFO:
info = VOSK_MODEL_INFO[size]
is_downloaded = _is_vosk_model_downloaded(size, self.language)
@@ -5472,6 +5509,9 @@ class SettingsDialog(Gtk.Dialog):
if engine == "parakeet":
model_id = self.model_combo.get_active_id()
return model_id.lower() if model_id else None
if engine == "faster_whisper":
model_id = self.model_combo.get_active_id()
return model_id.lower() if model_id else None
return None
def _list_unused_downloads(self) -> list[tuple[str, str, str]]:
@@ -5538,6 +5578,16 @@ class SettingsDialog(Gtk.Dialog):
name == active_id,
)
)
elif engine == "faster_whisper":
for name in list_downloaded_faster_whisper_models():
items.append(
(
name,
_model_display_name(name),
_format_size(FASTER_WHISPER_MODEL_INFO[name]["size_mb"]),
name == active_id,
)
)
return items
@@ -5624,6 +5674,8 @@ class SettingsDialog(Gtk.Dialog):
delete_vosk_model(model_id)
elif engine == "parakeet":
parakeet.delete_model(model_id)
elif engine == "faster_whisper":
delete_faster_whisper_model(model_id)
else:
raise ValueError(f"No local models to delete for engine {engine}")
@@ -5842,7 +5894,7 @@ class SettingsDialog(Gtk.Dialog):
continue
is_downloaded = _is_vosk_model_downloaded("small", lang_code)
display_text += " ✓" if is_downloaded else " ↓"
elif engine in ["whisper", "whisper_cpp", "parakeet", "remote_api"]:
elif engine in ["whisper", "whisper_cpp", "parakeet", "faster_whisper", "remote_api"]:
if english_only_whispercpp and lang_info.get("whisper") != "en":
continue
# Both Whisper and whisper.cpp support auto-detect
@@ -6284,6 +6336,14 @@ class SettingsDialog(Gtk.Dialog):
is_downloaded = parakeet.is_model_downloaded(model_name)
recommended, reason = parakeet.RECOMMENDED_MODEL, parakeet.RECOMMENDED_REASON
extra_info = f"Size: {_format_size(info['size_mb'])}"
elif engine == "faster_whisper":
if model_name not in FASTER_WHISPER_MODEL_INFO:
self.model_info_card.hide()
return
info = FASTER_WHISPER_MODEL_INFO[model_name]
is_downloaded = is_faster_whisper_model_downloaded(model_name)
recommended, reason = get_recommended_faster_whisper_model()
extra_info = f"Parameters: {info['params']}"
else:
self.model_info_card.hide()
return
@@ -6372,6 +6432,9 @@ class SettingsDialog(Gtk.Dialog):
elif engine == "parakeet" and not parakeet.is_model_downloaded(model_name):
needs_download = True
model_info = parakeet.PARAKEET_MODEL_INFO.get(model_name, {"size_mb": 639})
elif engine == "faster_whisper" and not is_faster_whisper_model_downloaded(model_name):
needs_download = True
model_info = FASTER_WHISPER_MODEL_INFO.get(model_name, {"size_mb": 39})
if needs_download:
if not self.speech_engine.try_begin_download():
@@ -6831,6 +6894,9 @@ For now, the engine has been reverted to VOSK."""
elif engine == "parakeet" and not parakeet.is_model_downloaded(model_name):
needs_download = True
model_info = parakeet.PARAKEET_MODEL_INFO.get(model_name, {"size_mb": 639})
elif engine == "faster_whisper" and not is_faster_whisper_model_downloaded(model_name):
needs_download = True
model_info = FASTER_WHISPER_MODEL_INFO.get(model_name, {"size_mb": 39})
if needs_download:
if not self.speech_engine.try_begin_download():
@@ -0,0 +1,275 @@
"""Faster-Whisper model information for Vocalinux.
Metadata and checksum-gated download URLs for the optional faster-whisper
backend (CTranslate2). Bundles are stored under ``models_dir()/faster_whisper/``
and each file is verified against ``model_checksums.txt`` before the engine
loads it, the same way Parakeet and whisper.cpp models are.
"""
import logging
import os
import shutil
from functools import lru_cache
from typing import Any, Dict, Optional
from .paths import is_within_directory, models_dir
logger = logging.getLogger(__name__)
_HF_BASE_URL = "https://huggingface.co"
# Each bundle is pinned to a Hugging Face commit rather than tracking main, so
# a file replaced upstream cannot change what an install downloads.
FASTER_WHISPER_MODEL_INFO: Dict[str, Dict[str, Any]] = {
"tiny": {
"repo": "Systran/faster-whisper-tiny",
"revision": "d90ca5fe260221311c53c58e660288d3deb8d356",
"size_mb": 39,
"params": "39M",
"desc": "Fastest, lowest accuracy",
},
"tiny.en": {
"repo": "Systran/faster-whisper-tiny.en",
"revision": "0d3d19a32d3338f10357c0889762bd8d64bbdeba",
"size_mb": 39,
"params": "39M",
"desc": "English-only tiny model",
},
"base": {
"repo": "Systran/faster-whisper-base",
"revision": "ebe41f70d5b6dfa9166e2c581c45c9c0cfc57b66",
"size_mb": 74,
"params": "74M",
"desc": "Fast, good for basic use",
},
"base.en": {
"repo": "Systran/faster-whisper-base.en",
"revision": "3d3d5dee26484f91867d81cb899cfcf72b96be6c",
"size_mb": 74,
"params": "74M",
"desc": "English-only base model",
},
"small": {
"repo": "Systran/faster-whisper-small",
"revision": "536b0662742c02347bc0e980a01041f333bce120",
"size_mb": 244,
"params": "244M",
"desc": "Balanced speed/accuracy",
},
"small.en": {
"repo": "Systran/faster-whisper-small.en",
"revision": "d1d751a5f8271d482d14ca55d9e2deeebbae577f",
"size_mb": 244,
"params": "244M",
"desc": "English-only small model",
},
"medium": {
"repo": "Systran/faster-whisper-medium",
"revision": "08e178d48790749d25932bbc082711ddcfdfbc4f",
"size_mb": 769,
"params": "769M",
"desc": "High accuracy, slower",
},
"medium.en": {
"repo": "Systran/faster-whisper-medium.en",
"revision": "a29b04bd15381511a9af671baec01072039215e3",
"size_mb": 769,
"params": "769M",
"desc": "English-only medium model",
},
"large-v1": {
"repo": "Systran/faster-whisper-large-v1",
"revision": "b07c8d4be0be90092aa01a29c975077acb8d15c9",
"size_mb": 1550,
"params": "1550M",
"desc": "Legacy large v1 model",
},
"large-v2": {
"repo": "Systran/faster-whisper-large-v2",
"revision": "f0fe81560cb8b68660e564f55dd99207059c092e",
"size_mb": 1550,
"params": "1550M",
"desc": "Legacy large v2 model",
},
"large-v3": {
"repo": "Systran/faster-whisper-large-v3",
"revision": "edaa852ec7e145841d8ffdb056a99866b5f0a478",
"size_mb": 1550,
"params": "1550M",
"desc": "Highest accuracy, slower",
},
}
_DEFAULT_MODEL_FILES = ["config.json", "model.bin", "tokenizer.json", "vocabulary.txt"]
_LARGE_V3_MODEL_FILES = [
"config.json",
"model.bin",
"preprocessor_config.json",
"tokenizer.json",
"vocabulary.json",
]
def model_files(model_name: str) -> list[str]:
"""Return the files that make up a faster-whisper CTranslate2 bundle."""
if model_name not in FASTER_WHISPER_MODEL_INFO:
raise ValueError(f"Unknown faster-whisper model: {model_name}")
if model_name == "large-v3":
return list(_LARGE_V3_MODEL_FILES)
return list(_DEFAULT_MODEL_FILES)
def get_model_path(model_name: str) -> str:
"""
Get the path where a model should be stored.
Args:
model_name: Name of the model (for example tiny)
Returns:
Path to the model directory
"""
faster_whisper_dir = os.path.join(models_dir(), "faster_whisper")
os.makedirs(faster_whisper_dir, exist_ok=True)
return os.path.join(faster_whisper_dir, model_name)
def is_model_downloaded(model_name: str) -> bool:
"""
Check if a faster-whisper model is downloaded.
Args:
model_name: Name of the model
Returns:
True if every bundle file exists under MODELS_DIR, False otherwise
"""
if model_name not in FASTER_WHISPER_MODEL_INFO:
return False
model_path = get_model_path(model_name)
return all(
os.path.exists(os.path.join(model_path, filename)) for filename in model_files(model_name)
)
def list_downloaded_models() -> list[str]:
"""Return faster-whisper models occupying disk, complete or partially downloaded.
Mirrors the Parakeet helper in listing anything present so Settings can offer
deletion; a cancelled download leaves files behind that must be reclaimable.
"""
return [name for name in FASTER_WHISPER_MODEL_INFO if os.path.isdir(get_model_path(name))]
def delete_model(model_name: str) -> str:
"""Delete a downloaded faster-whisper model directory.
Returns:
The removed filesystem path.
Raises:
ValueError: Unknown model name, or path outside the models directory.
FileNotFoundError: The model directory is not present.
OSError: The directory could not be removed.
"""
if model_name not in FASTER_WHISPER_MODEL_INFO or os.path.basename(model_name) != model_name:
raise ValueError(f"Unknown faster-whisper model: {model_name}")
path = get_model_path(model_name)
if not is_within_directory(path, models_dir()):
raise ValueError("Refusing to delete a path outside the models directory")
if not os.path.isdir(path):
raise FileNotFoundError(path)
shutil.rmtree(path)
logger.info("Deleted faster-whisper model %s (%s)", model_name, path)
return path
def manifest_key(model_name: str, filename: str) -> str:
"""Key this bundle file is pinned under in model_checksums.txt.
Several bundles ship the same file names, so the model name is part of the
key rather than the bare filename other engines use.
"""
return f"faster-whisper-{model_name}-{filename}"
def get_model_file_url(model_name: str, filename: str) -> str:
"""
Get the download URL for one file of a faster-whisper model.
Args:
model_name: Name of the model
filename: File within the model bundle
Returns:
Hugging Face download URL
"""
model_info = FASTER_WHISPER_MODEL_INFO.get(model_name)
if not model_info:
raise ValueError(f"Unknown faster-whisper model: {model_name}")
return (
f"{_HF_BASE_URL}/{model_info['repo']}/resolve/{model_info['revision']}"
f"/{filename}?download=true"
)
@lru_cache(maxsize=1)
def _has_torch_cuda() -> bool:
"""Return True if PyTorch reports CUDA available."""
try:
import torch
return bool(torch.cuda.is_available())
except Exception:
return False
def get_recommended_model() -> tuple[str, str]:
"""Get the recommended faster-whisper model based on system configuration.
Returns:
Tuple of (model_name, reason)
"""
try:
import psutil
ram_gb = int(psutil.virtual_memory().total) // (1024**3)
except Exception:
ram_gb = 4
has_cuda = _has_torch_cuda()
if has_cuda:
if ram_gb >= 8:
return "small", f"CUDA GPU with {ram_gb}GB RAM"
return "base", f"CUDA GPU with {ram_gb}GB RAM"
if ram_gb >= 16:
return "base", f"{ram_gb}GB RAM - CPU inference"
if ram_gb >= 8:
return "tiny", f"{ram_gb}GB RAM - optimized for speed"
return "tiny", f"Limited RAM ({ram_gb}GB) - fastest model"
def get_compute_type(device: str) -> str:
"""Return the recommended compute type for the given device.
Args:
device: "cpu" or "cuda"
Returns:
A faster-whisper compute_type value.
"""
if device == "cpu":
return "int8"
return "float16"
def is_english_only_model(model_name: str) -> bool:
"""Return whether a faster-whisper model variant is English-only."""
return ".en" in model_name.lower()
+125 -78
View File
@@ -2,7 +2,8 @@
#
# Generated by scripts/generate-model-checksums.py -- do not edit by hand.
# Regenerate with `just model-checksums` after adding a model to
# vosk_model_info.py, whispercpp_model_info.py or parakeet_model_info.py.
# vosk_model_info.py, whispercpp_model_info.py, parakeet_model_info.py
# or faster_whisper_model_info.py.
#
# Read by src/vocalinux/utils/model_checksums.py and by install.sh; keep the
# format (filename, algorithm, digest, size in bytes) parseable by awk.
@@ -17,81 +18,127 @@
# VOSK digests are the sha256 of bytes this script downloaded, not the md5
# Alphacephei publishes; that md5 is only checked against them at that point.
#
# Parakeet bundles are pinned per model in parakeet_model_info.py, so their
# keys carry the model name: several bundles ship the same file names.
# Parakeet and faster-whisper bundles are pinned per model in their
# model_info modules, so their keys carry the model name: several
# bundles ship the same file names.
ggml-base-q5_1.bin sha256 422f1ae452ade6f30a004d7e5c6a43195e4433bc370bf23fac9cc591f01a8898 59707625
ggml-base-q8_0.bin sha256 c577b9a86e7e048a0b7eada054f4dd79a56bbfa911fbdacf900ac5b567cbb7d9 81768585
ggml-base.bin sha256 60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe 147951465
ggml-base.en-q5_1.bin sha256 4baf70dd0d7c4247ba2b81fafd9c01005ac77c2f9ef064e00dcf195d0e2fdd2f 59721011
ggml-base.en.bin sha256 a03779c86df3323075f5e796cb2ce5029f00ec8869eee3fdfb897afe36c6d002 147964211
ggml-large-v1.bin sha256 7d99f41a10525d0206bddadd86760181fa920438b6b33237e3118ff6c83bb53d 3094623691
ggml-large-v2-q5_0.bin sha256 3a214837221e4530dbc1fe8d734f302af393eb30bd0ed046042ebf4baf70f6f2 1080732091
ggml-large-v2-q8_0.bin sha256 fef54e6d898246a65c8285bfa83bd1807e27fadf54d5d4e81754c47634737e8c 1656129691
ggml-large-v2.bin sha256 9a423fe4d40c82774b6af34115b8b935f34152246eb19e80e376071d3f999487 3094623691
ggml-large-v3-q5_0.bin sha256 d75795ecff3f83b5faa89d1900604ad8c780abd5739fae406de19f23ecd98ad1 1081140203
ggml-large-v3-turbo-q5_0.bin sha256 394221709cd5ad1f40c46e6031ca61bce88931e6e088c188294c6d5a55ffa7e2 574041195
ggml-large-v3-turbo-q8_0.bin sha256 317eb69c11673c9de1e1f0d459b253999804ec71ac4c23c17ecf5fbe24e259a1 874188075
ggml-large-v3-turbo.bin sha256 1fc70f774d38eb169993ac391eea357ef47c88757ef72ee5943879b7e8e2bc69 1624555275
ggml-large-v3.bin sha256 64d182b440b98d5203c4f9bd541544d84c605196c4f7b845dfa11fb23594d1e2 3095033483
ggml-medium-q5_0.bin sha256 19fea4b380c3a618ec4723c3eef2eb785ffba0d0538cf43f8f235e7b3b34220f 539212467
ggml-medium-q8_0.bin sha256 42a1ffcbe4167d224232443396968db4d02d4e8e87e213d3ee2e03095dea6502 823369779
ggml-medium.bin sha256 6c14d5adee5f86394037b4e4e8b59f1673b6cee10e3cf0b11bbdbee79c156208 1533763059
ggml-medium.en-q5_0.bin sha256 76733e26ad8fe1c7a5bf7531a9d41917b2adc0f20f2e4f5531688a8c6cd88eb0 539225533
ggml-medium.en.bin sha256 cc37e93478338ec7700281a7ac30a10128929eb8f427dda2e865faa8f6da4356 1533774781
ggml-small-q5_1.bin sha256 ae85e4a935d7a567bd102fe55afc16bb595bdb618e11b2fc7591bc08120411bb 190085487
ggml-small-q8_0.bin sha256 49c8fb02b65e6049d5fa6c04f81f53b867b5ec9540406812c643f177317f779f 264464607
ggml-small.bin sha256 1be3a9b2063867b937e64e2ec7483364a79917e157fa98c5d94b5c1fffea987b 487601967
ggml-small.en-q5_1.bin sha256 bfdff4894dcb76bbf647d56263ea2a96645423f1669176f4844a1bf8e478ad30 190098681
ggml-small.en.bin sha256 c6138d6d58ecc8322097e0f987c32f1be8bb0a18532a3f88f734d1bbf9c41e5d 487614201
ggml-tiny-q5_1.bin sha256 818710568da3ca15689e31a743197b520007872ff9576237bda97bd1b469c3d7 32152673
ggml-tiny-q8_0.bin sha256 c2085835d3f50733e2ff6e4b41ae8a2b8d8110461e18821b09a15c40c42d1cca 43537433
ggml-tiny.bin sha256 be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21 77691713
ggml-tiny.en-q5_1.bin sha256 c77c5766f1cef09b6b7d47f21b546cbddd4157886b3b5d6d4f709e91e66c7c2b 32166155
ggml-tiny.en.bin sha256 921e4cf8686fdd993dcd081a5da5b6c365bfde1162e72b08d75ac75289920b1f 77704715
vosk-model-ar-0.22-linto-1.1.0.zip sha256 6fc103417517203a5503e176963e3a7e20d32da4c37f4ff21181e9d64e1f02bf 1377395170
vosk-model-cn-0.22.zip sha256 7f5580bf7ca3d9e8ce8be68337378950ed78ef974df7a7d4ead8dbe32bec2fa1 1358736686
vosk-model-de-0.21.zip sha256 245060756f8d8394fc5b13639cf220b7620205795f30f37b6823878d4f603b2a 2031717803
vosk-model-en-in-0.5.zip sha256 0365c52f79308d81c2de832cf934ce42da6bf7e04dd3acd4203cacf50ab70f5c 971186187
vosk-model-en-us-0.22.zip sha256 47f9a81ebb039dbb0bd319175c36ac393c0893b796c2b6303e64cf58c27b69f6 1913365522
vosk-model-es-0.42.zip sha256 bbbfaf9239bed5dcb0d140886b5834fcc13c13c7fb2f2799ae790f03ff20bfce 1484681703
vosk-model-fa-0.42.zip sha256 5f03187838a187cd3f8766e3d329da37bf5ddd81a861081132743b76d8a8d14a 1667089770
vosk-model-fr-0.22.zip sha256 b26c34e5ea1b9f9dee6cef5e910a1d93e9adb60f1a0a3f85c069976aec481e46 1523026348
vosk-model-hi-0.22.zip sha256 eaf9bdbd703c79e957f7ea893502e12b336f98d7cd420138bcb224b36ccf86ac 1561909274
vosk-model-it-0.22.zip sha256 5cdc9f0f596e3db270b92fc4c1fd0f1a3e3d5d19ae0d30b69fc008639257de09 1306765292
vosk-model-ja-0.22.zip sha256 af620db815ef716f90355f911092ba0c7454d91d793ecdd8dee2df6342e0e9cc 1045975323
vosk-model-nl-spraakherkenning-0.6.zip sha256 088d40014c43684d65f3cc15f4b06ad764b4407b47854812e8364427702c8b56 1004206965
vosk-model-pt-fb-v0.1.1-pruned.zip sha256 f197db8d66a53e1ae4332dc599b4c035271a0ffe0c6ac0636f341184cea8d830 1223922169
vosk-model-ru-0.22.zip sha256 84d9e2cf6324480e4e768da7d75122fd1250df5449b89d8f9d9417ec7a27fb23 1559419132
vosk-model-small-ar-0.3.zip sha256 8696c91f39ec67a08c513845cfac26dbd4e81937be4fba25233e6c5435c18ca9 104351896
vosk-model-small-ca-0.4.zip sha256 99f90bfff5c2b187c705ddbc20aaa2600eddc359466f404404f1db6029fba5d4 43405881
vosk-model-small-cn-0.22.zip sha256 3af8b0e7e0f835ae9d414ce5df580237a3cfb08d586c9fbbb0f7ff29ad5b14ba 43898754
vosk-model-small-cs-0.4-rhasspy.zip sha256 287c3bbefc8ad67b4ab9636eecef3d62acc3719990777d03e226db5a7f19fbda 46088666
vosk-model-small-de-0.15.zip sha256 b7e53c90b1f0a38456f4cd62b366ecd58803cd97cd42b06438e2c131713d5e43 46499967
vosk-model-small-en-in-0.4.zip sha256 20663dcac4d5cb783a579c54d98339344a688e4ec6e1b4a4b059fd1235454cc7 37573330
vosk-model-small-en-us-0.15.zip sha256 30f26242c4eb449f948e42cb302dd7a686cb29a3423a8367f99ff41780942498 41205931
vosk-model-small-es-0.42.zip sha256 09b239888f633ef2f0b4e09736e3d9936acfd810bc65d53fad45261762c6511f 39817833
vosk-model-small-fa-0.42.zip sha256 977cb5faa538f3a835ccfd35f5f6d8284b5c450b89c700b9bd4736b66536ad46 53431220
vosk-model-small-fr-0.22.zip sha256 cabf6180e177eb9b3a9a9d43a437bd5e549f3a7d09525e5d69a3fed787be12ad 42233323
vosk-model-small-hi-0.22.zip sha256 7c50a10866889f0ac21d912c20537a055a597ed09fc1d3e5bcd798f9f0017e48 44458845
vosk-model-small-it-0.22.zip sha256 9ec65e75861d1c6c2e457cccd932705340dcdf233f5b239f00733b4de0bf3267 49665141
vosk-model-small-ja-0.22.zip sha256 efa092d280153a77615e9e0c7d7283e93e600de3d19d3bec686c57ef19d52eac 49704573
vosk-model-small-ko-0.22.zip sha256 eea36124087fed26c59996a4761519458e3bd185e8ea9d9865ad8760c4a1d989 86914329
vosk-model-small-nl-0.22.zip sha256 039811c3b829de64e4f123a9f684a53784005b212a346ac0b899dc7efce2ed0a 40441176
vosk-model-small-pl-0.22.zip sha256 c4cd16498ea544f446f9e9a55cbd602b71cfe5a2b6f2b0834d81e1b6fce15f0d 52979372
vosk-model-small-pt-0.3.zip sha256 6e1ce909032e1afa7a88e68a3d628ecafff302bdf195befab308826c395e93b7 32453112
vosk-model-small-ru-0.22.zip sha256 961d5ff98a17f4aa6de69864d0aa71fa5bac682301d2b5d17a3f24c5c99a46d4 46236750
vosk-model-small-sv-rhasspy-0.15.zip sha256 5b54f2ca8cacf9766588d05f6c8ca7e8921f054f050726e29d3516e78aefe054 303504931
vosk-model-small-tr-0.3.zip sha256 8c8d07cec1bce31add14967c16891c84152cdf76d391e33c08278119b9ea96e5 36855784
vosk-model-small-uk-v3-small.zip sha256 ee05b6a8c796c53c61cd1c821d38e897e5bd67b4d691d8797ec8f9e5100d4247 143914407
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-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
parakeet-v3-european-tokens.txt sha256 d58544679ea4bc6ac563d1f545eb7d474bd6cfa467f0a6e2c1dc1c7d37e3c35d 93939
parakeet-v2-english-encoder.int8.onnx sha256 a32b12d17bbbc309d0686fbbcc2987b5e9b8333a7da83fa6b089f0a2acd651ab 652184296
parakeet-v2-english-decoder.int8.onnx sha256 b6bb64963457237b900e496ee9994b59294526439fbcc1fecf705b31a15c6b4e 7257753
parakeet-v2-english-joiner.int8.onnx sha256 7946164367946e7f9f29a122407c3252b680dbae9a51343eb2488d057c3c43d2 1739080
parakeet-v2-english-tokens.txt sha256 ec182b70dd42113aff6c5372c75cac58c952443eb22322f57bbd7f53977d497d 9384
ggml-base-q5_1.bin sha256 422f1ae452ade6f30a004d7e5c6a43195e4433bc370bf23fac9cc591f01a8898 59707625
ggml-base-q8_0.bin sha256 c577b9a86e7e048a0b7eada054f4dd79a56bbfa911fbdacf900ac5b567cbb7d9 81768585
ggml-base.bin sha256 60ed5bc3dd14eea856493d334349b405782ddcaf0028d4b5df4088345fba2efe 147951465
ggml-base.en-q5_1.bin sha256 4baf70dd0d7c4247ba2b81fafd9c01005ac77c2f9ef064e00dcf195d0e2fdd2f 59721011
ggml-base.en.bin sha256 a03779c86df3323075f5e796cb2ce5029f00ec8869eee3fdfb897afe36c6d002 147964211
ggml-large-v1.bin sha256 7d99f41a10525d0206bddadd86760181fa920438b6b33237e3118ff6c83bb53d 3094623691
ggml-large-v2-q5_0.bin sha256 3a214837221e4530dbc1fe8d734f302af393eb30bd0ed046042ebf4baf70f6f2 1080732091
ggml-large-v2-q8_0.bin sha256 fef54e6d898246a65c8285bfa83bd1807e27fadf54d5d4e81754c47634737e8c 1656129691
ggml-large-v2.bin sha256 9a423fe4d40c82774b6af34115b8b935f34152246eb19e80e376071d3f999487 3094623691
ggml-large-v3-q5_0.bin sha256 d75795ecff3f83b5faa89d1900604ad8c780abd5739fae406de19f23ecd98ad1 1081140203
ggml-large-v3-turbo-q5_0.bin sha256 394221709cd5ad1f40c46e6031ca61bce88931e6e088c188294c6d5a55ffa7e2 574041195
ggml-large-v3-turbo-q8_0.bin sha256 317eb69c11673c9de1e1f0d459b253999804ec71ac4c23c17ecf5fbe24e259a1 874188075
ggml-large-v3-turbo.bin sha256 1fc70f774d38eb169993ac391eea357ef47c88757ef72ee5943879b7e8e2bc69 1624555275
ggml-large-v3.bin sha256 64d182b440b98d5203c4f9bd541544d84c605196c4f7b845dfa11fb23594d1e2 3095033483
ggml-medium-q5_0.bin sha256 19fea4b380c3a618ec4723c3eef2eb785ffba0d0538cf43f8f235e7b3b34220f 539212467
ggml-medium-q8_0.bin sha256 42a1ffcbe4167d224232443396968db4d02d4e8e87e213d3ee2e03095dea6502 823369779
ggml-medium.bin sha256 6c14d5adee5f86394037b4e4e8b59f1673b6cee10e3cf0b11bbdbee79c156208 1533763059
ggml-medium.en-q5_0.bin sha256 76733e26ad8fe1c7a5bf7531a9d41917b2adc0f20f2e4f5531688a8c6cd88eb0 539225533
ggml-medium.en.bin sha256 cc37e93478338ec7700281a7ac30a10128929eb8f427dda2e865faa8f6da4356 1533774781
ggml-small-q5_1.bin sha256 ae85e4a935d7a567bd102fe55afc16bb595bdb618e11b2fc7591bc08120411bb 190085487
ggml-small-q8_0.bin sha256 49c8fb02b65e6049d5fa6c04f81f53b867b5ec9540406812c643f177317f779f 264464607
ggml-small.bin sha256 1be3a9b2063867b937e64e2ec7483364a79917e157fa98c5d94b5c1fffea987b 487601967
ggml-small.en-q5_1.bin sha256 bfdff4894dcb76bbf647d56263ea2a96645423f1669176f4844a1bf8e478ad30 190098681
ggml-small.en.bin sha256 c6138d6d58ecc8322097e0f987c32f1be8bb0a18532a3f88f734d1bbf9c41e5d 487614201
ggml-tiny-q5_1.bin sha256 818710568da3ca15689e31a743197b520007872ff9576237bda97bd1b469c3d7 32152673
ggml-tiny-q8_0.bin sha256 c2085835d3f50733e2ff6e4b41ae8a2b8d8110461e18821b09a15c40c42d1cca 43537433
ggml-tiny.bin sha256 be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21 77691713
ggml-tiny.en-q5_1.bin sha256 c77c5766f1cef09b6b7d47f21b546cbddd4157886b3b5d6d4f709e91e66c7c2b 32166155
ggml-tiny.en.bin sha256 921e4cf8686fdd993dcd081a5da5b6c365bfde1162e72b08d75ac75289920b1f 77704715
vosk-model-ar-0.22-linto-1.1.0.zip sha256 6fc103417517203a5503e176963e3a7e20d32da4c37f4ff21181e9d64e1f02bf 1377395170
vosk-model-cn-0.22.zip sha256 7f5580bf7ca3d9e8ce8be68337378950ed78ef974df7a7d4ead8dbe32bec2fa1 1358736686
vosk-model-de-0.21.zip sha256 245060756f8d8394fc5b13639cf220b7620205795f30f37b6823878d4f603b2a 2031717803
vosk-model-en-in-0.5.zip sha256 0365c52f79308d81c2de832cf934ce42da6bf7e04dd3acd4203cacf50ab70f5c 971186187
vosk-model-en-us-0.22.zip sha256 47f9a81ebb039dbb0bd319175c36ac393c0893b796c2b6303e64cf58c27b69f6 1913365522
vosk-model-es-0.42.zip sha256 bbbfaf9239bed5dcb0d140886b5834fcc13c13c7fb2f2799ae790f03ff20bfce 1484681703
vosk-model-fa-0.42.zip sha256 5f03187838a187cd3f8766e3d329da37bf5ddd81a861081132743b76d8a8d14a 1667089770
vosk-model-fr-0.22.zip sha256 b26c34e5ea1b9f9dee6cef5e910a1d93e9adb60f1a0a3f85c069976aec481e46 1523026348
vosk-model-hi-0.22.zip sha256 eaf9bdbd703c79e957f7ea893502e12b336f98d7cd420138bcb224b36ccf86ac 1561909274
vosk-model-it-0.22.zip sha256 5cdc9f0f596e3db270b92fc4c1fd0f1a3e3d5d19ae0d30b69fc008639257de09 1306765292
vosk-model-ja-0.22.zip sha256 af620db815ef716f90355f911092ba0c7454d91d793ecdd8dee2df6342e0e9cc 1045975323
vosk-model-nl-spraakherkenning-0.6.zip sha256 088d40014c43684d65f3cc15f4b06ad764b4407b47854812e8364427702c8b56 1004206965
vosk-model-pt-fb-v0.1.1-pruned.zip sha256 f197db8d66a53e1ae4332dc599b4c035271a0ffe0c6ac0636f341184cea8d830 1223922169
vosk-model-ru-0.22.zip sha256 84d9e2cf6324480e4e768da7d75122fd1250df5449b89d8f9d9417ec7a27fb23 1559419132
vosk-model-small-ar-0.3.zip sha256 8696c91f39ec67a08c513845cfac26dbd4e81937be4fba25233e6c5435c18ca9 104351896
vosk-model-small-ca-0.4.zip sha256 99f90bfff5c2b187c705ddbc20aaa2600eddc359466f404404f1db6029fba5d4 43405881
vosk-model-small-cn-0.22.zip sha256 3af8b0e7e0f835ae9d414ce5df580237a3cfb08d586c9fbbb0f7ff29ad5b14ba 43898754
vosk-model-small-cs-0.4-rhasspy.zip sha256 287c3bbefc8ad67b4ab9636eecef3d62acc3719990777d03e226db5a7f19fbda 46088666
vosk-model-small-de-0.15.zip sha256 b7e53c90b1f0a38456f4cd62b366ecd58803cd97cd42b06438e2c131713d5e43 46499967
vosk-model-small-en-in-0.4.zip sha256 20663dcac4d5cb783a579c54d98339344a688e4ec6e1b4a4b059fd1235454cc7 37573330
vosk-model-small-en-us-0.15.zip sha256 30f26242c4eb449f948e42cb302dd7a686cb29a3423a8367f99ff41780942498 41205931
vosk-model-small-es-0.42.zip sha256 09b239888f633ef2f0b4e09736e3d9936acfd810bc65d53fad45261762c6511f 39817833
vosk-model-small-fa-0.42.zip sha256 977cb5faa538f3a835ccfd35f5f6d8284b5c450b89c700b9bd4736b66536ad46 53431220
vosk-model-small-fr-0.22.zip sha256 cabf6180e177eb9b3a9a9d43a437bd5e549f3a7d09525e5d69a3fed787be12ad 42233323
vosk-model-small-hi-0.22.zip sha256 7c50a10866889f0ac21d912c20537a055a597ed09fc1d3e5bcd798f9f0017e48 44458845
vosk-model-small-it-0.22.zip sha256 9ec65e75861d1c6c2e457cccd932705340dcdf233f5b239f00733b4de0bf3267 49665141
vosk-model-small-ja-0.22.zip sha256 efa092d280153a77615e9e0c7d7283e93e600de3d19d3bec686c57ef19d52eac 49704573
vosk-model-small-ko-0.22.zip sha256 eea36124087fed26c59996a4761519458e3bd185e8ea9d9865ad8760c4a1d989 86914329
vosk-model-small-nl-0.22.zip sha256 039811c3b829de64e4f123a9f684a53784005b212a346ac0b899dc7efce2ed0a 40441176
vosk-model-small-pl-0.22.zip sha256 c4cd16498ea544f446f9e9a55cbd602b71cfe5a2b6f2b0834d81e1b6fce15f0d 52979372
vosk-model-small-pt-0.3.zip sha256 6e1ce909032e1afa7a88e68a3d628ecafff302bdf195befab308826c395e93b7 32453112
vosk-model-small-ru-0.22.zip sha256 961d5ff98a17f4aa6de69864d0aa71fa5bac682301d2b5d17a3f24c5c99a46d4 46236750
vosk-model-small-sv-rhasspy-0.15.zip sha256 5b54f2ca8cacf9766588d05f6c8ca7e8921f054f050726e29d3516e78aefe054 303504931
vosk-model-small-tr-0.3.zip sha256 8c8d07cec1bce31add14967c16891c84152cdf76d391e33c08278119b9ea96e5 36855784
vosk-model-small-uk-v3-small.zip sha256 ee05b6a8c796c53c61cd1c821d38e897e5bd67b4d691d8797ec8f9e5100d4247 143914407
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-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
parakeet-v3-european-tokens.txt sha256 d58544679ea4bc6ac563d1f545eb7d474bd6cfa467f0a6e2c1dc1c7d37e3c35d 93939
parakeet-v2-english-encoder.int8.onnx sha256 a32b12d17bbbc309d0686fbbcc2987b5e9b8333a7da83fa6b089f0a2acd651ab 652184296
parakeet-v2-english-decoder.int8.onnx sha256 b6bb64963457237b900e496ee9994b59294526439fbcc1fecf705b31a15c6b4e 7257753
parakeet-v2-english-joiner.int8.onnx sha256 7946164367946e7f9f29a122407c3252b680dbae9a51343eb2488d057c3c43d2 1739080
parakeet-v2-english-tokens.txt sha256 ec182b70dd42113aff6c5372c75cac58c952443eb22322f57bbd7f53977d497d 9384
faster-whisper-tiny-config.json sha256 a73a28cdfe1c43ccc7202fa333d1f89c202477271407ae9a7f19afa52039cac8 2249
faster-whisper-tiny-model.bin sha256 dcb76c6586fc06cbdac6dd21f14cfd129cc4cdd9dce19bf4ffa62e59cbe6e6d1 75538270
faster-whisper-tiny-tokenizer.json sha256 fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab 2203239
faster-whisper-tiny-vocabulary.txt sha256 34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913 459861
faster-whisper-tiny.en-config.json sha256 14b1b421a90349bc551b881461426b561a874049cb9e4c4864f2ca384f6a7cc5 2317
faster-whisper-tiny.en-model.bin sha256 1a5afae06a4db91c975c9a9d78be5cc110ee4ea022ad57d55492e4550e936b2a 75537502
faster-whisper-tiny.en-tokenizer.json sha256 929c5252409436dce1b38a75d1abbcb5e132d170d8e324e4e04ed915fa2d22df 2128466
faster-whisper-tiny.en-vocabulary.txt sha256 ff77588746d3a2595d32ab5b69ffd7b95ce2441ac57533cb66fc3eb575a115cf 422309
faster-whisper-base-config.json sha256 56a6d8110d311f19c8f0471e562832c7527f146b567275bfca59fcf7c184da9a 2309
faster-whisper-base-model.bin sha256 d01c3014881c9c6f3133c182f3d2887eb6ca1c789a7538c5c007196857a0a6a9 145217532
faster-whisper-base-tokenizer.json sha256 fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab 2203239
faster-whisper-base-vocabulary.txt sha256 34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913 459861
faster-whisper-base.en-config.json sha256 f3bc3821e9fc76a27bae538e11ae5b677dcdd352b4600429ce7951d398569aeb 2227
faster-whisper-base.en-model.bin sha256 2a166925539a16005f14ff328359f9b9adb9dc4fb631bb3b227526862e93e2ef 145216508
faster-whisper-base.en-tokenizer.json sha256 929c5252409436dce1b38a75d1abbcb5e132d170d8e324e4e04ed915fa2d22df 2128466
faster-whisper-base.en-vocabulary.txt sha256 ff77588746d3a2595d32ab5b69ffd7b95ce2441ac57533cb66fc3eb575a115cf 422309
faster-whisper-small-config.json sha256 b55496ac7940a7ae47d2c01eab40edfd8701feec1229d9cce3b40014383fb828 2370
faster-whisper-small-model.bin sha256 3e305921506d8872816023e4c273e75d2419fb89b24da97b4fe7bce14170d671 483546902
faster-whisper-small-tokenizer.json sha256 fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab 2203239
faster-whisper-small-vocabulary.txt sha256 34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913 459861
faster-whisper-small.en-config.json sha256 666a9605530ac1f61fa8177f3702b4dacec9966749e42610839fcc32661d5fae 2657
faster-whisper-small.en-model.bin sha256 62b2a45b05ee59acb4a5341b33ee35e041395d378d418a18acfe4c9e768ee37a 483545366
faster-whisper-small.en-tokenizer.json sha256 929c5252409436dce1b38a75d1abbcb5e132d170d8e324e4e04ed915fa2d22df 2128466
faster-whisper-small.en-vocabulary.txt sha256 ff77588746d3a2595d32ab5b69ffd7b95ce2441ac57533cb66fc3eb575a115cf 422309
faster-whisper-medium-config.json sha256 3622a2ddc41ec0e0fd4e68c13c6830f03b90c38d89aaad184de02c8c642cf807 2257
faster-whisper-medium-model.bin sha256 9b45e1009dcc4ab601eff815b61d80e60ce3fd8c74c1a14f4a282258286b51ae 1527906378
faster-whisper-medium-tokenizer.json sha256 fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab 2203239
faster-whisper-medium-vocabulary.txt sha256 34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913 459861
faster-whisper-medium.en-config.json sha256 4a1848ebabe7938d9797c15a2e8e4ce1d36e6fd4a43d096ae5955257c67c7962 2643
faster-whisper-medium.en-model.bin sha256 11b220779aea4c6f3ce9d2549c8a95ea869ed84066864b999531ef53e594fe5b 1527904330
faster-whisper-medium.en-tokenizer.json sha256 929c5252409436dce1b38a75d1abbcb5e132d170d8e324e4e04ed915fa2d22df 2128466
faster-whisper-medium.en-vocabulary.txt sha256 ff77588746d3a2595d32ab5b69ffd7b95ce2441ac57533cb66fc3eb575a115cf 422309
faster-whisper-large-v1-config.json sha256 98eb866f6d98c38224aa2f13fc9854f75fa01280714e945f7e803aa8b16e2b4b 2352
faster-whisper-large-v1-model.bin sha256 a3cce8081a5414206ab09a80aa410ebf9965feef52adafeead13f4a83398b1d1 3086912962
faster-whisper-large-v1-tokenizer.json sha256 fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab 2203239
faster-whisper-large-v1-vocabulary.txt sha256 34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913 459861
faster-whisper-large-v2-config.json sha256 d86b7a7664a12559d644aa210a32ce9a7e03913e794b7ea4fb7182de69e273a7 2796
faster-whisper-large-v2-model.bin sha256 bf2a9746382e1aa7ffff6b3a0d137ed9edbd9670c3b87e5d35f5e85e70d0333a 3086912962
faster-whisper-large-v2-tokenizer.json sha256 fb7b63191e9bb045082c79fd742a3106a12c99513ab30df4a0d47fa6cb6fd0ab 2203239
faster-whisper-large-v2-vocabulary.txt sha256 34ce3fe1c5041027b3f8d42912270993f986dbc4bb34cf27f951e34a1e453913 459861
faster-whisper-large-v3-config.json sha256 a9306624f5ec14270a014b647e5c316b6e03a662c369758d1b90697a7b0655b9 2394
faster-whisper-large-v3-model.bin sha256 69f74147e3334731bc3a76048724833325d2ec74642fb52620eda87352e3d4f1 3087284237
faster-whisper-large-v3-preprocessor_config.json sha256 7ccc62c6f2765af1f3b46c00c9b5894426835a05021c8b9c01eecb6dfb542711 340
faster-whisper-large-v3-tokenizer.json sha256 6d8cbd7cd0d8d5815e478dac67b85a26bbe77c1f5e0c6d76d1ce2abc0e5f21ca 2480617
faster-whisper-large-v3-vocabulary.json sha256 c69260f2ab26d659b7c398f9a2b2b48ed0df16c3b47d7326782fd9cba71690c1 1068114
+3
View File
@@ -458,11 +458,14 @@ class TestUnloadModelAndAutoPauseBlock(unittest.TestCase):
mgr._model_lock.__enter__ = MagicMock(return_value=None)
mgr._model_lock.__exit__ = MagicMock(return_value=False)
mgr._http_session = None
mgr._faster_whisper_engine = None
mgr._reconnection_attempts = 0
mgr._update_state = MagicMock()
mgr._init_vosk = MagicMock()
mgr._init_whisper = MagicMock()
mgr._init_whispercpp = MagicMock()
mgr._init_parakeet = MagicMock()
mgr._init_faster_whisper = MagicMock()
mgr._init_remote_api = MagicMock()
mgr.stop_recognition = MagicMock()
return mgr
+4 -1
View File
@@ -187,7 +187,10 @@ class TestConfigManager(unittest.TestCase):
# Also test the get method works - it should return a valid engine value
config_manager = ConfigManager()
value = config_manager.get("speech_recognition", "engine")
self.assertIn(value, ["vosk", "whisper", "whisper_cpp"]) # Should be one of valid engines
self.assertIn(
value,
["vosk", "whisper", "whisper_cpp", "parakeet", "faster_whisper", "remote_api"],
)
self.assertEqual(
config_manager.get("speech_recognition", "remote_api_model"),
"whisper-1",
+555
View File
@@ -0,0 +1,555 @@
"""Tests for the faster-whisper engine backend."""
import os
import sys
from unittest.mock import MagicMock, patch
import numpy as np
import pytest
sys.modules["gi"] = MagicMock()
sys.modules["gi.repository"] = MagicMock()
from vocalinux.common_types import Engine, EngineType
from vocalinux.speech_recognition.engines.faster_whisper_engine import FasterWhisperEngine
from vocalinux.utils.faster_whisper_model_info import (
FASTER_WHISPER_MODEL_INFO,
get_compute_type,
get_model_file_url,
get_model_path,
get_recommended_model,
is_english_only_model,
is_model_downloaded,
manifest_key,
model_files,
)
class TestEngineType:
"""Tests for the EngineType enum."""
def test_engine_type_values(self):
"""Test that all expected engine types exist with correct string values."""
assert EngineType.VOSK.value == "vosk"
assert EngineType.WHISPER.value == "whisper"
assert EngineType.WHISPER_CPP.value == "whisper_cpp"
assert EngineType.PARAKEET.value == "parakeet"
assert EngineType.FASTER_WHISPER.value == "faster_whisper"
assert EngineType.REMOTE_API.value == "remote_api"
class TestEngineProtocol:
"""Tests for the Engine Protocol."""
def test_engine_protocol_methods(self):
"""Test that the Engine Protocol defines the expected methods."""
mock_engine = MagicMock(spec=Engine)
mock_engine.init()
mock_engine.is_ready()
mock_engine.transcribe([])
mock_engine.cleanup()
mock_engine.init.assert_called_once()
mock_engine.is_ready.assert_called_once()
mock_engine.transcribe.assert_called_once_with([])
mock_engine.cleanup.assert_called_once()
class TestFasterWhisperModelInfo:
"""Tests for faster-whisper model metadata utilities."""
def test_model_info_keys(self):
"""Test that expected model sizes are defined."""
for size in ["tiny", "base", "small", "medium", "large-v3"]:
assert size in FASTER_WHISPER_MODEL_INFO
def test_get_compute_type(self):
"""Test compute type recommendations."""
assert get_compute_type("cpu") == "int8"
assert get_compute_type("cuda") == "float16"
def test_is_english_only_model(self):
"""Test English-only model detection."""
assert is_english_only_model("tiny.en") is True
assert is_english_only_model("tiny") is False
def test_is_model_downloaded_unknown_model(self):
"""Test that unknown models are reported as not downloaded."""
assert is_model_downloaded("not-a-model") is False
def test_model_files_default_and_large_v3(self):
"""Most bundles use vocabulary.txt; large-v3 uses vocabulary.json."""
assert model_files("tiny") == [
"config.json",
"model.bin",
"tokenizer.json",
"vocabulary.txt",
]
assert model_files("large-v3") == [
"config.json",
"model.bin",
"preprocessor_config.json",
"tokenizer.json",
"vocabulary.json",
]
def test_model_files_unknown_model(self):
with pytest.raises(ValueError, match="Unknown faster-whisper model"):
model_files("not-a-model")
def test_manifest_key_includes_model_name(self):
assert manifest_key("tiny", "model.bin") == "faster-whisper-tiny-model.bin"
assert (
manifest_key("large-v3", "vocabulary.json") == "faster-whisper-large-v3-vocabulary.json"
)
def test_get_model_file_url_uses_pinned_revision(self):
url = get_model_file_url("tiny", "model.bin")
revision = FASTER_WHISPER_MODEL_INFO["tiny"]["revision"]
assert f"/resolve/{revision}/model.bin?download=true" in url
assert "Systran/faster-whisper-tiny" in url
def test_get_model_file_url_unknown_model(self):
with pytest.raises(ValueError, match="Unknown faster-whisper model"):
get_model_file_url("not-a-model", "model.bin")
def test_is_model_downloaded_missing_files(self, tmp_path):
"""A partial bundle is not treated as downloaded."""
with patch(
"vocalinux.utils.faster_whisper_model_info.models_dir",
return_value=str(tmp_path),
):
model_dir = get_model_path("tiny")
os.makedirs(model_dir, exist_ok=True)
with open(os.path.join(model_dir, "model.bin"), "wb") as handle:
handle.write(b"partial")
assert is_model_downloaded("tiny") is False
def test_is_model_downloaded_complete_bundle(self, tmp_path):
"""All bundle files under MODELS_DIR count as downloaded."""
with patch(
"vocalinux.utils.faster_whisper_model_info.models_dir",
return_value=str(tmp_path),
):
model_dir = get_model_path("tiny")
os.makedirs(model_dir, exist_ok=True)
for filename in model_files("tiny"):
with open(os.path.join(model_dir, filename), "wb") as handle:
handle.write(b"ok")
assert is_model_downloaded("tiny") is True
def test_get_recommended_model_returns_tuple(self):
"""Test that get_recommended_model returns a model name and reason."""
model, reason = get_recommended_model()
assert isinstance(model, str)
assert isinstance(reason, str)
assert model in FASTER_WHISPER_MODEL_INFO
def test_get_recommended_model_cuda_high_ram(self):
"""Test CUDA recommendation with high RAM."""
with patch(
"vocalinux.utils.faster_whisper_model_info._has_torch_cuda",
return_value=True,
):
with patch("psutil.virtual_memory") as mock_mem:
mock_mem.return_value.total = 16 * 1024**3
model, _reason = get_recommended_model()
assert model == "small"
def test_get_recommended_model_cpu_low_ram(self):
"""Test CPU recommendation with low RAM."""
with patch(
"vocalinux.utils.faster_whisper_model_info._has_torch_cuda",
return_value=False,
):
with patch("psutil.virtual_memory") as mock_mem:
mock_mem.return_value.total = 4 * 1024**3
model, _reason = get_recommended_model()
assert model == "tiny"
def test_get_recommended_model_cpu_medium_ram(self):
"""Test CPU recommendation with medium RAM."""
with patch(
"vocalinux.utils.faster_whisper_model_info._has_torch_cuda",
return_value=False,
):
with patch("psutil.virtual_memory") as mock_mem:
mock_mem.return_value.total = 8 * 1024**3
model, _reason = get_recommended_model()
assert model == "tiny"
def test_get_recommended_model_cpu_high_ram(self):
"""Test CPU recommendation with high RAM."""
with patch(
"vocalinux.utils.faster_whisper_model_info._has_torch_cuda",
return_value=False,
):
with patch("psutil.virtual_memory") as mock_mem:
mock_mem.return_value.total = 32 * 1024**3
model, _reason = get_recommended_model()
assert model == "base"
def test_get_recommended_model_cuda_low_ram(self):
"""Test CUDA recommendation with low VRAM-system RAM."""
with patch(
"vocalinux.utils.faster_whisper_model_info._has_torch_cuda",
return_value=True,
):
with patch("psutil.virtual_memory") as mock_mem:
mock_mem.return_value.total = 4 * 1024**3
model, _reason = get_recommended_model()
assert model == "base"
def test_has_torch_cuda_available(self):
"""Test that torch detection reports CUDA availability."""
from vocalinux.utils.faster_whisper_model_info import _has_torch_cuda
torch_mock = MagicMock()
torch_mock.cuda.is_available.return_value = True
_has_torch_cuda.cache_clear()
try:
with patch.dict(sys.modules, {"torch": torch_mock}):
assert _has_torch_cuda() is True
finally:
_has_torch_cuda.cache_clear()
_has_torch_cuda()
def test_has_torch_cuda_exception(self):
"""Test that torch detection falls back to False on errors."""
from vocalinux.utils.faster_whisper_model_info import _has_torch_cuda
_has_torch_cuda.cache_clear()
try:
with patch.dict(sys.modules, {"torch": None}):
with patch("builtins.__import__", side_effect=ImportError("not found")):
assert _has_torch_cuda() is False
finally:
_has_torch_cuda.cache_clear()
_has_torch_cuda()
def test_get_recommended_model_psutil_exception(self):
"""Test that psutil failures default to a safe model choice."""
with patch(
"vocalinux.utils.faster_whisper_model_info._has_torch_cuda",
return_value=False,
):
with patch("psutil.virtual_memory", side_effect=RuntimeError("boom")):
model, _reason = get_recommended_model()
assert model == "tiny"
class TestFasterWhisperEngine:
"""Tests for the FasterWhisperEngine class."""
@pytest.fixture(autouse=True)
def _stub_model_path(self, tmp_path):
with patch(
"vocalinux.speech_recognition.engines.faster_whisper_engine.get_model_path",
side_effect=lambda name: str(tmp_path / "faster_whisper" / name),
):
self._models_root = tmp_path
yield
def _mock_whisper_model(self, segments):
"""Return a mock WhisperModel that yields the given segments."""
whisper_mock = MagicMock()
whisper_mock.WhisperModel.return_value.transcribe.return_value = (
segments,
MagicMock(),
)
return whisper_mock
def test_engine_implements_protocol(self):
"""Test that FasterWhisperEngine satisfies the Engine Protocol."""
engine = FasterWhisperEngine(model_size="tiny")
assert hasattr(engine, "init")
assert hasattr(engine, "transcribe")
assert hasattr(engine, "is_ready")
assert hasattr(engine, "cleanup")
def test_default_model_size(self):
"""Test that the engine defaults to a valid model size."""
engine = FasterWhisperEngine()
assert engine.model_size in FASTER_WHISPER_MODEL_INFO
def test_init_loads_model(self):
"""Test that init() creates a WhisperModel instance."""
whisper_mock = self._mock_whisper_model([])
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
assert engine.is_ready()
whisper_mock.WhisperModel.assert_called_once_with(
str(self._models_root / "faster_whisper" / "tiny"),
device="cpu",
compute_type="int8",
local_files_only=True,
)
def test_init_invalid_model_defaults_to_tiny(self):
"""Test that an invalid model size is corrected to tiny."""
whisper_mock = self._mock_whisper_model([])
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="invalid", device="cpu")
engine.init()
assert engine.model_size == "tiny"
def test_transcribe_returns_text(self):
"""Test that transcription concatenates segment text."""
segments = [
MagicMock(text="Hello,"),
MagicMock(text=" world."),
]
whisper_mock = self._mock_whisper_model(segments)
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
audio = np.array([0, 1000, -1000, 0], dtype=np.int16)
audio_bytes = [audio.tobytes()]
text = engine.transcribe(audio_bytes)
assert text == "Hello, world."
def test_transcribe_empty_audio(self):
"""Test that empty audio returns empty text."""
whisper_mock = self._mock_whisper_model([])
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
assert engine.transcribe([]) == ""
def test_transcribe_not_ready(self):
"""Test that transcription before init returns empty text."""
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
assert engine.transcribe([b""]) == ""
def test_cleanup_releases_model(self):
"""Test that cleanup() resets the engine state."""
whisper_mock = self._mock_whisper_model([])
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
assert engine.is_ready()
engine.cleanup()
assert not engine.is_ready()
def test_device_detects_cuda(self):
"""Test that CUDA is detected when torch reports it available."""
from vocalinux.utils.faster_whisper_model_info import _has_torch_cuda
torch_mock = MagicMock()
torch_mock.cuda.is_available.return_value = True
_has_torch_cuda.cache_clear()
try:
with patch.dict(sys.modules, {"torch": torch_mock}):
engine = FasterWhisperEngine(device=None)
assert engine.device == "cuda"
finally:
_has_torch_cuda.cache_clear()
_has_torch_cuda()
def test_device_falls_back_to_cpu(self):
"""Test CPU fallback when torch is unavailable."""
from vocalinux.utils.faster_whisper_model_info import _has_torch_cuda
_has_torch_cuda.cache_clear()
try:
with patch.dict(sys.modules, {"torch": None}):
engine = FasterWhisperEngine(device=None)
assert engine.device == "cpu"
finally:
_has_torch_cuda.cache_clear()
_has_torch_cuda()
def test_init_import_error(self):
"""Test that import failure propagates correctly."""
with patch.dict(sys.modules, {"faster_whisper": None}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
with patch("builtins.__import__", side_effect=ImportError("not found")):
with pytest.raises(ImportError):
engine.init()
def test_init_already_initialized(self):
"""Test that init() is idempotent."""
whisper_mock = self._mock_whisper_model([])
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
engine.init()
whisper_mock.WhisperModel.assert_called_once()
def test_transcribe_handles_error(self):
"""Test that transcription errors return empty text."""
whisper_mock = MagicMock()
whisper_mock.WhisperModel.return_value.transcribe.side_effect = RuntimeError("boom")
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
audio = np.array([0, 1000], dtype=np.int16)
text = engine.transcribe([audio.tobytes()])
assert text == ""
def test_transcribe_empty_result(self):
"""Test that empty segment results return empty text and log debug."""
whisper_mock = self._mock_whisper_model([MagicMock(text="")])
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
engine = FasterWhisperEngine(model_size="tiny", device="cpu")
engine.init()
audio = np.array([0, 1000], dtype=np.int16)
text = engine.transcribe([audio.tobytes()])
assert text == ""
def test_language_mapping(self):
"""Test that Vocalinux language codes are mapped correctly."""
engine = FasterWhisperEngine(language="en-us")
assert engine._normalize_language() == "en"
engine = FasterWhisperEngine(language="auto")
assert engine._normalize_language() is None
engine = FasterWhisperEngine(language="fr")
assert engine._normalize_language() == "fr"
class TestEngineRegistry:
"""Tests for the speech recognition engine registry."""
def test_registry_contains_faster_whisper(self):
"""Test that the faster-whisper engine is registered."""
from vocalinux.common_types import EngineType
from vocalinux.speech_recognition.engines import ENGINES
assert EngineType.FASTER_WHISPER in ENGINES
def test_registry_ignores_import_errors(self):
"""Test that the registry stays empty if the engine module fails to import."""
import importlib
import types
import vocalinux.speech_recognition.engines as registry_module
engine_module_name = "vocalinux.speech_recognition.engines.faster_whisper_engine"
real_engine_module = sys.modules.get(engine_module_name)
fake_module = types.ModuleType(engine_module_name)
fake_module.__getattr__ = lambda _name: (_ for _ in ()).throw(ImportError("not found"))
sys.modules[engine_module_name] = fake_module
try:
importlib.reload(registry_module)
assert EngineType.FASTER_WHISPER not in registry_module.ENGINES
finally:
if real_engine_module is not None:
sys.modules[engine_module_name] = real_engine_module
else:
sys.modules.pop(engine_module_name, None)
importlib.reload(registry_module)
class TestRecognitionManagerIntegration:
"""Tests that the recognition manager exposes faster-whisper methods."""
def test_faster_whisper_methods_exist(self):
"""Test that the manager has faster-whisper init and transcribe methods."""
whisper_mock = MagicMock()
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
assert hasattr(SpeechRecognitionManager, "_init_faster_whisper")
assert hasattr(SpeechRecognitionManager, "_transcribe_with_faster_whisper")
def test_faster_whisper_supported_in_config(self):
"""Test that the config manager recognizes faster-whisper model size."""
from vocalinux.ui.config_manager import ConfigManager
manager = ConfigManager()
size = manager.get_model_size_for_engine("faster_whisper")
assert size in FASTER_WHISPER_MODEL_INFO
def test_manager_init_faster_whisper(self):
"""Test that the manager can initialize with faster-whisper engine."""
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
whisper_mock = MagicMock()
with patch.dict(sys.modules, {"faster_whisper": whisper_mock}):
with (
patch.object(SpeechRecognitionManager, "_init_vosk"),
patch.object(SpeechRecognitionManager, "_init_whisper"),
patch.object(SpeechRecognitionManager, "_init_whispercpp"),
patch.object(SpeechRecognitionManager, "_init_parakeet"),
):
manager = SpeechRecognitionManager(
engine="faster_whisper",
model_size="tiny",
language="en-us",
defer_download=True,
)
assert manager.engine == "faster_whisper"
def test_init_faster_whisper_defers_missing_model(self):
"""Missing models stay unloaded when download is deferred."""
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
with (
patch.object(SpeechRecognitionManager, "_init_vosk"),
patch.object(SpeechRecognitionManager, "_init_whisper"),
patch.object(SpeechRecognitionManager, "_init_whispercpp"),
patch.object(SpeechRecognitionManager, "_init_parakeet"),
patch(
"vocalinux.speech_recognition.recognition_manager.faster_whisper.is_model_downloaded",
return_value=False,
),
patch.object(SpeechRecognitionManager, "_download_faster_whisper_model") as download,
):
manager = SpeechRecognitionManager(
engine="faster_whisper",
model_size="tiny",
language="en-us",
defer_download=True,
)
assert manager.engine == "faster_whisper"
assert manager._model_initialized is False
assert manager._faster_whisper_engine is None
download.assert_not_called()
def test_init_faster_whisper_downloads_then_loads(self):
"""force_download path acquires the bundle, then loads the local snapshot."""
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
engine_instance = MagicMock()
engine_instance._model = MagicMock()
engine_cls = MagicMock(return_value=engine_instance)
with (
patch.object(SpeechRecognitionManager, "_init_vosk"),
patch.object(SpeechRecognitionManager, "_init_whisper"),
patch.object(SpeechRecognitionManager, "_init_whispercpp"),
patch.object(SpeechRecognitionManager, "_init_parakeet"),
patch(
"vocalinux.speech_recognition.recognition_manager.faster_whisper.is_model_downloaded",
return_value=False,
),
patch(
"vocalinux.speech_recognition.engines.faster_whisper_engine.FasterWhisperEngine",
engine_cls,
),
):
manager = SpeechRecognitionManager(
engine="faster_whisper",
model_size="tiny",
language="en-us",
defer_download=True,
)
manager._defer_download = False
with patch.object(manager, "_download_faster_whisper_model") as download:
manager._init_faster_whisper()
download.assert_called_once()
engine_cls.assert_called_once()
engine_instance.init.assert_called_once()
assert manager._model_initialized is True
assert manager._faster_whisper_engine is engine_instance
if __name__ == "__main__":
pytest.main([__file__, "-v"])
+1 -1
View File
@@ -33,7 +33,7 @@ def _venv_python(venv_dir: Path, importable: "set[str] | None" = None) -> Path:
``importable`` names the engine modules that succeed; every other engine
module fails. Anything else (the json probes) runs on the real interpreter.
"""
modules = {"vosk", "whisper", "pywhispercpp.model"}
modules = {"vosk", "whisper", "pywhispercpp.model", "sherpa_onnx", "faster_whisper"}
ok = importable if importable is not None else set()
python = venv_dir / "bin" / "python"
python.parent.mkdir(parents=True, exist_ok=True)
+20
View File
@@ -17,6 +17,11 @@ from tempfile import TemporaryDirectory
from unittest.mock import patch
from vocalinux.utils import model_checksums
from vocalinux.utils.faster_whisper_model_info import (
FASTER_WHISPER_MODEL_INFO,
)
from vocalinux.utils.faster_whisper_model_info import manifest_key as faster_whisper_manifest_key
from vocalinux.utils.faster_whisper_model_info import model_files as faster_whisper_model_files
from vocalinux.utils.model_checksums import (
VERIFICATION_STAMP_NAME,
ChecksumError,
@@ -89,6 +94,21 @@ class TestManifestCoverage(unittest.TestCase):
"run `just model-checksums` to refresh the manifest",
)
def test_every_faster_whisper_model_is_pinned(self):
pinned = pinned_filenames()
missing = [
faster_whisper_manifest_key(name, filename)
for name in FASTER_WHISPER_MODEL_INFO
for filename in faster_whisper_model_files(name)
if faster_whisper_manifest_key(name, filename) not in pinned
]
self.assertEqual(
missing,
[],
"faster-whisper models without a pinned checksum; "
"run `just model-checksums` to refresh the manifest",
)
def test_manifest_entries_are_well_formed(self):
for filename in pinned_filenames():
expected = expected_for(filename)
+41
View File
@@ -85,6 +85,47 @@ def test_vosk_delete_rejects_unknown_and_missing(tmp_path):
delete_vosk_model("vosk-model-small-en-us-0.15")
def test_faster_whisper_list_and_delete(tmp_path):
with patch("vocalinux.utils.faster_whisper_model_info.models_dir", return_value=str(tmp_path)):
from vocalinux.utils.faster_whisper_model_info import (
delete_model,
get_model_path,
list_downloaded_models,
model_files,
)
assert list_downloaded_models() == []
tiny = get_model_path("tiny")
os.makedirs(tiny, exist_ok=True)
for filename in model_files("tiny"):
with open(os.path.join(tiny, filename), "wb") as handle:
handle.write(b"tiny")
base = get_model_path("base")
os.makedirs(base, exist_ok=True)
with open(os.path.join(base, "model.bin"), "wb") as handle:
handle.write(b"partial")
downloaded = list_downloaded_models()
assert downloaded == ["tiny", "base"]
deleted = delete_model("tiny")
assert deleted == tiny
assert not os.path.exists(tiny)
assert os.path.exists(base)
assert list_downloaded_models() == ["base"]
def test_faster_whisper_delete_unknown_and_missing(tmp_path):
with patch("vocalinux.utils.faster_whisper_model_info.models_dir", return_value=str(tmp_path)):
from vocalinux.utils.faster_whisper_model_info import delete_model
with pytest.raises(ValueError, match="Unknown faster-whisper model"):
delete_model("not-a-real-model")
with pytest.raises(FileNotFoundError):
delete_model("tiny")
def test_whisper_list_and_delete(tmp_path):
cache = tmp_path / "whisper"
cache.mkdir()
+9 -1
View File
@@ -373,12 +373,20 @@ def test_engine_without_a_handler_keeps_its_own_notification():
def _reconfigure_manager():
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
manager_class = SpeechRecognitionManager
manager = MagicMock()
manager.engine = "whisper_cpp"
manager.model_size = "tiny"
manager.language = "en-us"
manager._defer_download = True
return SpeechRecognitionManager, manager
# reconfigure() dispatches through _init_selected_engine(); bind the real
# method so force_reinit still reaches _init_whispercpp on this mock.
manager._init_selected_engine = lambda: manager_class._init_selected_engine(manager)
manager._snapshot_reconfigure_state = lambda: manager_class._snapshot_reconfigure_state(manager)
manager._restore_reconfigure_state = lambda previous: manager_class._restore_reconfigure_state(
manager, previous
)
return manager_class, manager
def test_force_reinit_reinitializes_an_engine_that_did_not_change():
+10 -6
View File
@@ -51,12 +51,16 @@ for _k, _v in _ORIG_MODULES.items():
def _make_manager(engine="whisper_cpp", **kw):
"""Create a manager with deferred init to avoid actually loading models."""
with patch.object(SpeechRecognitionManager, "_init_vosk"):
with patch.object(SpeechRecognitionManager, "_init_whisper"):
with patch.object(SpeechRecognitionManager, "_init_whispercpp"):
mgr = SpeechRecognitionManager(
engine=engine, model_size="small", language="en-us", defer_download=True, **kw
)
with (
patch.object(SpeechRecognitionManager, "_init_vosk"),
patch.object(SpeechRecognitionManager, "_init_whisper"),
patch.object(SpeechRecognitionManager, "_init_whispercpp"),
patch.object(SpeechRecognitionManager, "_init_parakeet"),
patch.object(SpeechRecognitionManager, "_init_faster_whisper"),
):
mgr = SpeechRecognitionManager(
engine=engine, model_size="small", language="en-us", defer_download=True, **kw
)
return mgr
+285 -12
View File
@@ -23,12 +23,11 @@ if "gi" not in sys.modules:
if "gi.repository" not in sys.modules:
sys.modules["gi.repository"] = MagicMock()
from vocalinux.common_types import RecognitionState
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
from vocalinux.utils.model_checksums import (
VERIFICATION_STAMP_NAME,
ChecksumError,
expected_for,
)
from vocalinux.utils.faster_whisper_model_info import manifest_key as faster_whisper_manifest_key
from vocalinux.utils.faster_whisper_model_info import model_files as faster_whisper_model_files
from vocalinux.utils.model_checksums import VERIFICATION_STAMP_NAME, ChecksumError, expected_for
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
@@ -40,13 +39,14 @@ def _make_manager(engine="whisper_cpp", **kw):
with patch.object(SpeechRecognitionManager, "_init_whisper"):
with patch.object(SpeechRecognitionManager, "_init_whispercpp"):
with patch.object(SpeechRecognitionManager, "_init_parakeet"):
mgr = SpeechRecognitionManager(
engine=engine,
model_size="small",
language="en-us",
defer_download=True,
**kw,
)
with patch.object(SpeechRecognitionManager, "_init_faster_whisper"):
mgr = SpeechRecognitionManager(
engine=engine,
model_size="small",
language="en-us",
defer_download=True,
**kw,
)
# Ensure vosk_model_map is set (normally done in _init_vosk)
if not hasattr(mgr, "vosk_model_map"):
# The names _init_vosk() would pick for en-us. They have to be
@@ -804,6 +804,279 @@ class TestParakeetDownloadVerifiesExistingFiles:
assert encoder.read_bytes() == b"good-enough"
class TestFasterWhisperRejectsAnUnverifiedModelOnDisk:
"""Wiring test: the hash has to happen where the model is picked up."""
@staticmethod
def _write_bundle(tmp_path, payload=b"not the model that is pinned"):
model_dir = tmp_path / "tiny"
model_dir.mkdir()
for name in faster_whisper_model_files("tiny"):
(model_dir / name).write_bytes(payload)
return model_dir
def test_a_model_that_fails_its_pin_is_removed_and_not_loaded(self, tmp_path):
model_dir = self._write_bundle(tmp_path)
manager = _make_manager(engine="faster_whisper")
manager.model_size = "tiny"
manager._defer_download = True
with patch(
"vocalinux.speech_recognition.recognition_manager.faster_whisper.get_model_path",
return_value=str(model_dir),
):
with patch(
"vocalinux.speech_recognition.engines.faster_whisper_engine.FasterWhisperEngine"
) as engine_cls:
manager._init_faster_whisper()
for name in faster_whisper_model_files("tiny"):
assert not (model_dir / name).exists(), "an unverifiable model must not stay on disk"
engine_cls.assert_not_called()
assert manager._model_initialized is False
def test_a_model_that_cannot_be_deleted_is_still_not_loaded(self, tmp_path):
model_dir = self._write_bundle(tmp_path)
manager = _make_manager(engine="faster_whisper")
manager.model_size = "tiny"
manager._defer_download = False
with patch(
"vocalinux.speech_recognition.recognition_manager.faster_whisper.get_model_path",
return_value=str(model_dir),
):
with patch(
"vocalinux.speech_recognition.recognition_manager.os.remove",
side_effect=OSError("read-only"),
):
with patch(
"vocalinux.speech_recognition.engines.faster_whisper_engine.FasterWhisperEngine"
) as engine_cls:
with pytest.raises(RuntimeError, match="failed verification"):
manager._init_faster_whisper()
for name in faster_whisper_model_files("tiny"):
assert (model_dir / name).exists()
engine_cls.assert_not_called()
def test_an_unpinned_model_is_refused_not_deleted(self, tmp_path):
"""faster-whisper pins are constructed keys; simulate a missing pin for one file."""
model_dir = self._write_bundle(tmp_path, payload=b"bytes")
unpinned_name = faster_whisper_model_files("tiny")[0]
unpinned_key = faster_whisper_manifest_key("tiny", unpinned_name)
def fake_verify(path, filename=None):
key = filename or os.path.basename(path)
if key == unpinned_key:
raise ChecksumError(f"No checksum is pinned for {key}")
verify_model_file_real(path, filename)
def fake_expected(filename):
if os.path.basename(filename) == unpinned_key:
return None
return expected_for(filename)
manager = _make_manager(engine="faster_whisper")
manager.model_size = "tiny"
manager._defer_download = True
with patch(
"vocalinux.speech_recognition.recognition_manager.faster_whisper.get_model_path",
return_value=str(model_dir),
):
with patch(
"vocalinux.speech_recognition.recognition_manager.verify_model_file",
side_effect=fake_verify,
):
with patch(
"vocalinux.speech_recognition.recognition_manager.expected_for",
side_effect=fake_expected,
):
with patch(
"vocalinux.speech_recognition.engines.faster_whisper_engine.FasterWhisperEngine"
) as engine_cls:
manager._init_faster_whisper()
assert (
model_dir / unpinned_name
).exists(), "a missing pin is not a reason to delete the file"
engine_cls.assert_not_called()
assert manager._model_initialized is False
class TestFasterWhisperDownloadVerifiesExistingFiles:
"""Existence is not a pin: a leftover dest must still match its digest."""
def test_an_existing_file_that_fails_its_pin_is_redownloaded(self, tmp_path):
manager = _make_manager(engine="faster_whisper")
manager.model_size = "tiny"
model_dir = tmp_path / "tiny"
model_dir.mkdir()
first_name = faster_whisper_model_files("tiny")[0]
first_file = model_dir / first_name
first_file.write_bytes(b"not the file that is pinned")
first_key = faster_whisper_manifest_key("tiny", first_name)
streamed = []
verify_calls = []
def fake_stream(url, dest_path):
streamed.append(dest_path)
with open(dest_path, "wb") as handle:
handle.write(b"good-enough")
def fake_verify(path, filename=None):
verify_calls.append((path, filename))
if not str(path).endswith(".tmp"):
raise ChecksumError("digest mismatch")
mock_requests = MagicMock()
mock_requests.exceptions.RequestException = Exception
with patch.dict("sys.modules", {"requests": mock_requests}):
with patch(
"vocalinux.speech_recognition.recognition_manager.faster_whisper.get_model_path",
return_value=str(model_dir),
):
with patch.object(manager, "_stream_model_download", side_effect=fake_stream):
with patch(
"vocalinux.speech_recognition.recognition_manager.verify_model_file",
side_effect=fake_verify,
):
manager._download_faster_whisper_model()
assert any(
path == str(first_file) and filename == first_key for path, filename in verify_calls
), "an existing dest must be hashed, not skipped because it is already on disk"
assert any(
os.path.basename(path) == first_name + ".tmp" for path in streamed
), "an existing bad file must be removed and re-downloaded"
assert first_file.read_bytes() == b"good-enough"
class TestFailedReconfigureRestoresPreviousEngine:
"""A failed faster-whisper switch must not discard the live engine.
Settings reverts the pickers to the saved config on error. If reconfigure
keeps faster-whisper in ERROR after releasing whisper.cpp, dictation is
dead until restart even though the UI shows the old engine.
"""
def test_failed_faster_whisper_download_reloads_previous_engine(self):
manager = _make_manager(engine="whisper_cpp")
manager.engine = "whisper_cpp"
manager.model_size = "tiny"
manager._model_initialized = True
manager.state = RecognitionState.IDLE
with patch.object(
manager, "_init_faster_whisper", side_effect=RuntimeError("download failed")
):
with patch.object(manager, "_init_whispercpp") as restore_init:
with pytest.raises(RuntimeError, match="download failed"):
manager.reconfigure(
engine="faster_whisper",
model_size="tiny",
force_download=True,
)
restore_init.assert_called_once()
assert manager.engine == "whisper_cpp"
assert manager.model_size == "tiny"
assert manager.state != RecognitionState.ERROR
def test_restore_failure_stays_in_error(self):
manager = _make_manager(engine="whisper_cpp")
manager.engine = "whisper_cpp"
manager.model_size = "tiny"
manager.state = RecognitionState.IDLE
with patch.object(
manager, "_init_faster_whisper", side_effect=RuntimeError("download failed")
):
with patch.object(
manager, "_init_whispercpp", side_effect=RuntimeError("restore failed")
):
with pytest.raises(RuntimeError, match="download failed"):
manager.reconfigure(
engine="faster_whisper",
model_size="tiny",
force_download=True,
)
assert manager.engine == "whisper_cpp"
assert manager.state == RecognitionState.ERROR
def test_failed_switch_restores_live_settings_not_just_engine(self):
"""Unsaved VAD/device/API fields must roll back with the previous engine."""
manager = _make_manager(engine="whisper_cpp")
manager.engine = "whisper_cpp"
manager.model_size = "tiny"
manager.language = "en-us"
manager.vad_sensitivity = 2
manager.silence_timeout = 1.5
manager.audio_device_index = 1
manager.audio_device_name = "Built-in Mic"
manager._voice_commands_preference = False
manager.stop_sound_guard_ms = 200
manager.whispercpp_n_threads = 4
manager.whispercpp_gpu_device = 0
manager.whispercpp_no_timestamps = True
manager.remote_api_url = "http://old"
manager.remote_api_key = "old-key"
manager.remote_api_endpoint = "/inference"
manager.remote_api_model = "whisper-1"
manager._model_initialized = True
manager.state = RecognitionState.IDLE
with patch.object(
manager, "_init_faster_whisper", side_effect=RuntimeError("download failed")
):
with patch.object(manager, "_init_whispercpp") as restore_init:
with pytest.raises(RuntimeError, match="download failed"):
manager.reconfigure(
engine="faster_whisper",
model_size="tiny",
vad_sensitivity=5,
silence_timeout=4.0,
audio_device_index=9,
audio_device_name="USB Mic",
voice_commands_enabled=True,
stop_sound_guard_ms=50,
whispercpp_n_threads=1,
whispercpp_gpu_device=2,
whispercpp_no_timestamps=False,
remote_api_url="http://new",
remote_api_key="new-key",
remote_api_endpoint="/v1/audio",
remote_api_model="sensevoice",
force_download=True,
)
restore_init.assert_called_once()
assert manager.engine == "whisper_cpp"
assert manager.model_size == "tiny"
assert manager.language == "en-us"
assert manager.vad_sensitivity == 2
assert manager.silence_timeout == 1.5
assert manager.audio_device_index == 1
assert manager.audio_device_name == "Built-in Mic"
assert manager._voice_commands_preference is False
assert manager._voice_commands_enabled is False
assert manager.stop_sound_guard_ms == 200
assert manager.whispercpp_n_threads == 4
assert manager.whispercpp_gpu_device == 0
assert manager.whispercpp_no_timestamps is True
assert manager.remote_api_url == "http://old"
assert manager.remote_api_key == "old-key"
assert manager.remote_api_endpoint == "/inference"
assert manager.remote_api_model == "whisper-1"
assert manager.state != RecognitionState.ERROR
class TestAudioReconnection:
"""Test audio reconnection logic."""
+19 -6
View File
@@ -28,12 +28,20 @@ def _make_manager(engine="whisper_cpp", **kw):
"""Helper to create SpeechRecognitionManager with init methods patched."""
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager
with patch.object(SpeechRecognitionManager, "_init_vosk"):
with patch.object(SpeechRecognitionManager, "_init_whisper"):
with patch.object(SpeechRecognitionManager, "_init_whispercpp"):
return SpeechRecognitionManager(
engine=engine, model_size="small", language="en-us", defer_download=True, **kw
)
with (
patch.object(SpeechRecognitionManager, "_init_vosk"),
patch.object(SpeechRecognitionManager, "_init_whisper"),
patch.object(SpeechRecognitionManager, "_init_whispercpp"),
patch.object(SpeechRecognitionManager, "_init_parakeet"),
patch.object(SpeechRecognitionManager, "_init_faster_whisper"),
):
return SpeechRecognitionManager(
engine=engine,
model_size="small",
language="en-us",
defer_download=True,
**kw,
)
class TestSpeechRecognitionManagerInit:
@@ -56,6 +64,11 @@ class TestSpeechRecognitionManagerInit:
manager = _make_manager(engine="whisper")
assert manager.engine == "whisper"
def test_manager_init_faster_whisper_engine(self):
"""Test manager initialization with faster_whisper engine."""
manager = _make_manager(engine="faster_whisper")
assert manager.engine == "faster_whisper"
def test_manager_init_with_audio_device_index(self):
"""Test manager initialization with audio device index."""
manager = _make_manager(audio_device_index=2)
+19
View File
@@ -215,11 +215,14 @@ class TestReinitializeAfterResume(unittest.TestCase):
mgr._model_lock.__enter__ = MagicMock(return_value=None)
mgr._model_lock.__exit__ = MagicMock(return_value=False)
mgr._http_session = None
mgr._faster_whisper_engine = None
mgr._reconnection_attempts = 3
mgr._update_state = MagicMock()
mgr._init_vosk = MagicMock()
mgr._init_whisper = MagicMock()
mgr._init_whispercpp = MagicMock()
mgr._init_parakeet = MagicMock()
mgr._init_faster_whisper = MagicMock()
mgr.stop_recognition = MagicMock()
return mgr
@@ -247,6 +250,22 @@ class TestReinitializeAfterResume(unittest.TestCase):
mgr._init_whispercpp.assert_called_once()
def test_reinitializes_parakeet_engine(self):
mgr = self._make_manager()
mgr.engine = "parakeet"
mgr.reinitialize_after_resume()
mgr._init_parakeet.assert_called_once()
def test_reinitializes_faster_whisper_engine(self):
mgr = self._make_manager()
mgr.engine = "faster_whisper"
mgr.reinitialize_after_resume()
mgr._init_faster_whisper.assert_called_once()
def test_stops_active_recognition_first(self):
mgr = self._make_manager()
mgr.state = RecognitionState.LISTENING
Generated
+209 -1
View File
@@ -16,6 +16,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" },
]
[[package]]
name = "anyio"
version = "4.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "typing-extensions", marker = "python_full_version < '3.15'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" },
]
[[package]]
name = "ast-serialize"
version = "0.8.0"
@@ -80,6 +93,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/25/6c/b400476d3ceba681ab929787edc9554f6d88fcc69435eb681b00fc0457a5/ast_serialize-0.8.0-cp39-abi3-win_arm64.whl", hash = "sha256:b2a5978662fd4db463dfb4b974d2b10ac6430b98f5333aabc7051909df3561d0", size = 1083655, upload-time = "2026-08-07T11:29:00.349Z" },
]
[[package]]
name = "av"
version = "18.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8d/f4/f22114d30d3435e38c6af2b4870f37b864403dca6ae7af747a289ce0a18e/av-18.1.0.tar.gz", hash = "sha256:47bfc286e1bc9de7ab4681fc2b575cd2460a66919d31ffe1bd5aa54fae531a28", size = 4451061, upload-time = "2026-08-12T22:28:18.761Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/d4/d7cdc8bff143c17a6d35924375ae28dd692cacde38700a7d419fde54f44a/av-18.1.0-cp311-abi3-macosx_11_0_x86_64.whl", hash = "sha256:ae75d8bb6467895ed1f8572ededf7ffa49eac07f6e483222f5d7d62a41d12f04", size = 22546147, upload-time = "2026-08-12T22:27:11.851Z" },
{ url = "https://files.pythonhosted.org/packages/3f/c9/37a619297492256b77d5ed906e7d8166c10a26ed251dccf1ae03ab19bff6/av-18.1.0-cp311-abi3-macosx_14_0_arm64.whl", hash = "sha256:b30a4e8d934558e19602b68998a4d9ac9f250fa0dacef216f7e8e40153b13316", size = 18217603, upload-time = "2026-08-12T22:27:14.713Z" },
{ url = "https://files.pythonhosted.org/packages/d9/84/2464ffb64c08c5ce8b522c8e74594714414e3b0575267652c5c51c0574b9/av-18.1.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6fc837cc51adf80331ac850779cd53b5d4c4460b0ebe9057a02a921c6736f19d", size = 33640142, upload-time = "2026-08-12T22:27:17.835Z" },
{ url = "https://files.pythonhosted.org/packages/27/3a/204dbfc3e08eb4cdc6e6ff57be02150bc44523ebdb50182d10025792ebd9/av-18.1.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:8a032e8d8ebc73dec079364b9b4a6837638a2d106e8472314e685ffbf163e700", size = 35786210, upload-time = "2026-08-12T22:27:20.984Z" },
{ url = "https://files.pythonhosted.org/packages/e1/99/b0d04ec553ff9a7e00455458dfa3a39c8a8f627b273056b4e5fe57d590de/av-18.1.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:3c8b1f8b46f99d52e2d8b0ed5d0cdadf172d24794d46e2077b16e44ed08e26ff", size = 39379798, upload-time = "2026-08-12T22:27:24.432Z" },
{ url = "https://files.pythonhosted.org/packages/56/b1/e00d4feae59160149df6126585e726fdc6300798fd40c5dd324879e81f68/av-18.1.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ab5ac081bc9eaf54109120d4e56284674fecfbe520d9aa1707c7fa911ec5f4d2", size = 34690321, upload-time = "2026-08-12T22:27:27.769Z" },
{ url = "https://files.pythonhosted.org/packages/dc/94/836fa987e3084d11a21489f11357fb24843ef3aa8faf74ddddfc603d5062/av-18.1.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:191224788d87af06c31784a395bb73f14b72f33d7f4871ace0157de2abdc6276", size = 36859932, upload-time = "2026-08-12T22:27:31.403Z" },
{ url = "https://files.pythonhosted.org/packages/33/b4/76ba21e46704f632004276b85289a1582e95f5eff760436d6149875a1881/av-18.1.0-cp311-abi3-win_amd64.whl", hash = "sha256:ea1480b7a8d5405cb5f382b344731bf125fd2c1c6fae3964f6c48595628387ff", size = 27595679, upload-time = "2026-08-12T22:27:35.177Z" },
{ url = "https://files.pythonhosted.org/packages/4f/ad/a3135884c5753b09773176b97201ae602f67ad14206c395ff838d66bf9b0/av-18.1.0-cp311-abi3-win_arm64.whl", hash = "sha256:5509ec12aaa19fd6601de13cfa6f4cdad450da07982118510592875d970454d6", size = 20257584, upload-time = "2026-08-12T22:27:38.472Z" },
{ url = "https://files.pythonhosted.org/packages/4f/5b/4a756265d7fb164336c8d377bca21c39cfa2c178be23cedee840a69b59c5/av-18.1.0-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:b36b0bae9e4c62f9487c99481ec15e4e3870fcc868522cd6d18fc2d6bfa04f01", size = 22795654, upload-time = "2026-08-12T22:27:42.016Z" },
{ url = "https://files.pythonhosted.org/packages/d5/cc/1bc841462114a1adf4f7d87456ab78a6972e23271e71865fcd2bbd0e7360/av-18.1.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:025f84494cb23278498f03b0d8117d3e47a1cbc9c44b97eb31875cf02251e46b", size = 18435735, upload-time = "2026-08-12T22:27:45.787Z" },
{ url = "https://files.pythonhosted.org/packages/b8/20/005500ed17a2e62a5e4bb94aa3786942560ec2f55ec1895ebf174c87abef/av-18.1.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:08a9ae288299cfcbf739dba4ad0c53b9b71f45184303dd45947920d022fed695", size = 37090807, upload-time = "2026-08-12T22:27:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/5c/f7/11e7f6d848d3690c31ca4f8578167393e619177f1493ccc93b9400852d4e/av-18.1.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:cf8a17466bef07765dbdecc9e66ed9b25d20b4e14f654fbf35345a58ac45fa0c", size = 38976836, upload-time = "2026-08-12T22:27:54.565Z" },
{ url = "https://files.pythonhosted.org/packages/c3/63/b271473b24e806062d31191e40c6d65545e9cf59f80f044eba56dcbba0f4/av-18.1.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d49a5c542dfdc00f43c6cdb6cc41dac1781ee206fe180b56aa7433dfa816dfae", size = 40896630, upload-time = "2026-08-12T22:27:59.118Z" },
{ url = "https://files.pythonhosted.org/packages/6b/9f/2ab7fa292a947ad3466ed8e655eefa3b82f535d7ea598c297b4471a937c4/av-18.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5548b79e2bf1f59b3e9aedc918a72d9dc45b9adaac10ff9470d5dbdda0002e47", size = 37895673, upload-time = "2026-08-12T22:28:03.98Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d8/04507c57249b399c3e4f23f01d221532f357338b5316fd2858fbd343127d/av-18.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7ea063f6690193ea335a1d592d6e0274350d45e2ed6af83ee107cb90cbfd84f", size = 39992431, upload-time = "2026-08-12T22:28:08.736Z" },
{ url = "https://files.pythonhosted.org/packages/d6/d6/bc4b95bea9c2353a7e4d62a3fcfad9adcf0f881741c6ce01ee179d539ce3/av-18.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e4d48b9f12cad009cc72fe4f4099107de5e819c95f82767f4fd01a01481c0661", size = 28497798, upload-time = "2026-08-12T22:28:13.003Z" },
{ url = "https://files.pythonhosted.org/packages/c1/d2/0c277a46f12647c1833f40496e132fb6001e0d19e6144b5ea30896461feb/av-18.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5cd9085028902c9880622bd37a12fd4b33060f06a52311f6f4867ca9f29a2c3b", size = 21421979, upload-time = "2026-08-12T22:28:16.48Z" },
]
[[package]]
name = "babel"
version = "2.18.0"
@@ -529,6 +568,43 @@ toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "ctranslate2"
version = "4.8.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
{ name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "pyyaml" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/17/f22fbbb0723891704af803e1e7af5541bb46013adb347f870cbd8c4834d3/ctranslate2-4.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb2a5b4c206b3bd1f7f4b44d07e86e42050af6753053cdc86444f8d0ead9d6d8", size = 1271641, upload-time = "2026-08-31T19:37:07.757Z" },
{ url = "https://files.pythonhosted.org/packages/b2/af/cfe767012a0809c15155ecae3125ca66a16aeee4669bae146220cc631a76/ctranslate2-4.8.2-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:8221875b09ef982e3579a5165f8c9fcfb3d5de0f710af228eca2f73d69635ace", size = 11929414, upload-time = "2026-08-31T19:37:09.435Z" },
{ url = "https://files.pythonhosted.org/packages/10/2e/2d08b1219303af7256aeb78b98ee847ffb229c5fe4815e6d3a1b6483846a/ctranslate2-4.8.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3823c9883c2c410a76b2f19feda9da628a3c112cedfd912db815e0055c8235e2", size = 16720002, upload-time = "2026-08-31T19:37:11.957Z" },
{ url = "https://files.pythonhosted.org/packages/f3/3d/75c029ffb484957f1d2be0f9c3b9b1474955bb16c22df85bcba50c48df11/ctranslate2-4.8.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a24e0a95151b970941867fec94c983df978630f1996242a587c072d455607d28", size = 39378274, upload-time = "2026-08-31T19:37:14.898Z" },
{ url = "https://files.pythonhosted.org/packages/7e/86/21926da682103a20d4f4833b389131b801c71e6b8d6047be5f0a28fe673a/ctranslate2-4.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:995938fcd24a1174a7abf9765e7fa216b5b91a1d8e8c4c8f383c7a186e8bab2e", size = 19220522, upload-time = "2026-08-31T19:37:17.845Z" },
{ url = "https://files.pythonhosted.org/packages/12/97/9c63a51a8c8e13e95ec2aec639460fb0f271b0421e1a365b8aad440385c4/ctranslate2-4.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fedda669421a57f8164568ee86ea265727016988d310fcc2e24ca30e9ec457cd", size = 1271613, upload-time = "2026-08-31T19:37:19.958Z" },
{ url = "https://files.pythonhosted.org/packages/ca/27/e419389fb6040a8170bb30c3dcd50c29b70638852a264acc5bf804bf8800/ctranslate2-4.8.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:5385f15493e7b6f41377da83d0d5afeb8e0f36188260722f3800c3c7121455af", size = 11931717, upload-time = "2026-08-31T19:37:21.897Z" },
{ url = "https://files.pythonhosted.org/packages/53/46/bfa42114fd583b0ef30b67113486ab72d3ff466e60a824739257bcc533bd/ctranslate2-4.8.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:781f835da7fdc4adcc38f9d674dbe9b37eaf3c9aee3b2bc17d684342ff59d549", size = 16894628, upload-time = "2026-08-31T19:37:24.328Z" },
{ url = "https://files.pythonhosted.org/packages/01/23/d72e70cac2b7c5c832a629c380235b53b20fac8c0921856bcf625b08ba44/ctranslate2-4.8.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:554a0b2abf098a11c66ef7930c0a05b3de683a455e8ea3f2e6ab6966aaabc136", size = 39555908, upload-time = "2026-08-31T19:37:27.948Z" },
{ url = "https://files.pythonhosted.org/packages/4e/23/e3b5322ff7368fcbed181ea4c209149416e7940b5b04971d5ee4084afe1a/ctranslate2-4.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:d94421d565d0de61c032998f737a18942b0f2bef40c0424b1846ec6f67300105", size = 19222069, upload-time = "2026-08-31T19:37:31.531Z" },
{ url = "https://files.pythonhosted.org/packages/2c/00/3f5d12d94daa22ddf51de0252313476c0f8fca8b1c7776592953764a8b85/ctranslate2-4.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a40b190248389e1adb38ff90407937131c6bb9d7f9771f29ca1dca01946ff252", size = 1271572, upload-time = "2026-08-31T19:37:33.476Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b5/fde838502472462f2c8fd9e8fe17e734854bbe1047452c28d4fbe3eab7b2/ctranslate2-4.8.2-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:2778eaf89340062feb0d2d3dd35bedb14644673ccbce8934782dc8d093b2cab9", size = 11931591, upload-time = "2026-08-31T19:37:35.016Z" },
{ url = "https://files.pythonhosted.org/packages/a6/c7/cf22407330c90c1f3ef642ece11ff6bfe686509e995b95545951edf29117/ctranslate2-4.8.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:249b35f575adf8dd11e6ba43b212d5cbfc845d1140abc610c2872eef1ffb3481", size = 16895415, upload-time = "2026-08-31T19:37:37.342Z" },
{ url = "https://files.pythonhosted.org/packages/89/8b/051962470b8e9df1a43f0c9c7a03ce1c8a279db76be4e6a6fcb5171d1d29/ctranslate2-4.8.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:633f19d1ff8d053a8b179ca742b242002ac7882a2ab44e319c6efcde27d586a4", size = 39555517, upload-time = "2026-08-31T19:37:40.87Z" },
{ url = "https://files.pythonhosted.org/packages/c2/fc/a9e9e0ce1c0a29bc4c17bf56ccb4274293c0bbb2b8aae561727d82fcb0ca/ctranslate2-4.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:399c20a7336b6358f69ce3c615e090eabc08f29735a079fd1ac1e464119e0861", size = 19222061, upload-time = "2026-08-31T19:37:43.754Z" },
{ url = "https://files.pythonhosted.org/packages/72/b4/28111e86927b2490388d8e0b79ec83db38f2a81bf452318f24563b42e205/ctranslate2-4.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e32ae5e625a8999749745de43c2de5c12ceebb6d0d9cc9a3a956e8912bd68edb", size = 1272406, upload-time = "2026-08-31T19:37:45.671Z" },
{ url = "https://files.pythonhosted.org/packages/03/07/444fccd37ba9e3d84dcb985fc174c82636e7a8c69b47bb0c1bf566045d37/ctranslate2-4.8.2-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:980f13e1d41987908af945861081158fbcb2983f4f9399514adc8c87378b5587", size = 11932080, upload-time = "2026-08-31T19:37:47.24Z" },
{ url = "https://files.pythonhosted.org/packages/f9/c5/c8d91fa1848f9bc09e943337b9c7ebd20d77f1344f5d595d10979f5e59d4/ctranslate2-4.8.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e866ccf5f5668f7302c39b535d19d7e5811f3f00d6246faf6ed6fa070fa1af3", size = 16884555, upload-time = "2026-08-31T19:37:50.337Z" },
{ url = "https://files.pythonhosted.org/packages/14/0c/de0305052c2bb35979c4a6c668c7aa6f139c10bc0939833450b462cef829/ctranslate2-4.8.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573dcf5d96034dd9bcf7ee0db1657af490a52292ffc77dfb4b104da1e93322ad", size = 39521307, upload-time = "2026-08-31T19:37:53.623Z" },
{ url = "https://files.pythonhosted.org/packages/f2/86/03386a60b634a34c820e00f82bec808d38841cc2c007ec6b797892a9a5b5/ctranslate2-4.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:00bc9f44172d05bd2becefe757d2397ab3292fad1695169f9ebb9c16eb497ce2", size = 19478465, upload-time = "2026-08-31T19:37:56.488Z" },
{ url = "https://files.pythonhosted.org/packages/33/46/f25a66e905d3ce13aba221e6e7d63e24acd2539e48549675e3e0527f9ffe/ctranslate2-4.8.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58b1a78d050a7f281907b8acfe6f0cdbbf2feaf7464f38eb05b846f83fbaa297", size = 1295017, upload-time = "2026-08-31T19:37:58.379Z" },
{ url = "https://files.pythonhosted.org/packages/28/c2/0fc88102f448aee8fa704fec13e568f716df11aa701e901b2e401aa5d8ab/ctranslate2-4.8.2-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:70e2388d31dfb59968e6e011d01a6fc6bf802c0e1ad920a8d2a413f1c9da02ae", size = 11953132, upload-time = "2026-08-31T19:38:00.199Z" },
{ url = "https://files.pythonhosted.org/packages/62/23/f859ee8af1c6366795d59979a7e941a397823cc38585f5731c34a66f1048/ctranslate2-4.8.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32b9d0e984aa17a0da9b37bb7e4dcc638e898a2c32de08d5a4db53f15f640faa", size = 16872477, upload-time = "2026-08-31T19:38:02.641Z" },
{ url = "https://files.pythonhosted.org/packages/63/87/b964f427fcdfad983977859386007e1367eb340f747f63b6c0001cf78905/ctranslate2-4.8.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd9ab8b521230e8e962a58362b1ccde582806e781af8c1b9d7b244f3024468b1", size = 39491894, upload-time = "2026-08-31T19:38:06.365Z" },
{ url = "https://files.pythonhosted.org/packages/6a/39/9d316f00f184cea15e807a977df5bc76fc8f27f81246015f12083f42cd1c/ctranslate2-4.8.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f6f0b576c247984d3fc299a372ccc9319b668d6d25b0539f3c861beba15d0504", size = 19500896, upload-time = "2026-08-31T19:38:09.111Z" },
]
[[package]]
name = "cuda-bindings"
version = "13.3.1"
@@ -630,6 +706,22 @@ version = "1.9.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a5/f5/397b61091120a9ca5001041dd7bf76c385b3bfd67a0e5bcb74b852bd22a4/evdev-1.9.3.tar.gz", hash = "sha256:2c140e01ac8437758fa23fe5c871397412461f42d421aa20241dc8fe8cfccbc9", size = 32723, upload-time = "2026-02-05T21:54:24.987Z" }
[[package]]
name = "faster-whisper"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "av" },
{ name = "ctranslate2" },
{ name = "huggingface-hub" },
{ name = "onnxruntime" },
{ name = "tokenizers" },
{ name = "tqdm" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/99/49ee85903dee060d9f08297b4a342e5e0bcfca2f027a07b4ee0a38ab13f9/faster_whisper-1.2.1-py3-none-any.whl", hash = "sha256:79a66ad50688c0b794dd501dc340a736992a6342f7f95e5811be60b5224a26a7", size = 1118909, upload-time = "2025-10-31T11:35:47.794Z" },
]
[[package]]
name = "filelock"
version = "3.32.3"
@@ -670,6 +762,87 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/3c/6a2bf344106328fd04963664a60b9bb6496fc25df8e962fcdc1367285fb9/fsspec-2026.7.0-py3-none-any.whl", hash = "sha256:b57ddbafedfaef7018c1ecab32aa200a9d7ca26b77965f64e48b70061249d279", size = 206583, upload-time = "2026-07-28T16:34:49.538Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "hf-xet"
version = "1.6.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/ab/522a2ab67f27971a9d48ca666d4fca85ef7d5282d142e31fd087e27b1bbe/hf_xet-1.6.0.tar.gz", hash = "sha256:2e58454a340b3556dfa4972d5451aff4fba8dd42a236600ba1a1d2b1514f0fef", size = 920527, upload-time = "2026-08-03T22:33:13.243Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/41/62/3c062f593bd92ef4e77a0ef39541e3d82a0a1d3947c8a777a02a13a27828/hf_xet-1.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:70cbb9c896901600128cb9b6f06e132954fbede1db30f31f7c6c63f84cb7c31d", size = 4074584, upload-time = "2026-08-03T22:32:47.364Z" },
{ url = "https://files.pythonhosted.org/packages/bb/1e/c0ad437dd267a8e435bef594acf781bbc3874ff0b6435b4962d03ecf7cc4/hf_xet-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23379c2f9ec8696d952b16414a2bae72cad86a52df869b050698ba60f538c675", size = 3867381, upload-time = "2026-08-03T22:32:49.049Z" },
{ url = "https://files.pythonhosted.org/packages/d5/ee/7c0d7b6ab336167531b1c30af2af003f054af4c749becbd7209ae33a77c3/hf_xet-1.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f7278c05c22fd60cb436cda1269649b3e81db65ecdc8496e5e164aa4143e7b", size = 4453982, upload-time = "2026-08-03T22:32:50.568Z" },
{ url = "https://files.pythonhosted.org/packages/63/06/ad8eab1c9525246650cbaa821caa3cdbaca734ab1a5b8c91bea09cbd8d69/hf_xet-1.6.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:948f15d3a9545cfe5932f6bd8b440f6ae630aee108f14b7bd6c561f7c2dcc522", size = 4249445, upload-time = "2026-08-03T22:32:52.391Z" },
{ url = "https://files.pythonhosted.org/packages/d8/26/1eee8aedb0dafc1ab9717dc9ac602cde33361b232dc06803f1f6ed18b58c/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5153e6bb103ad49d6ea9f1b2e230db5a2ea32551ad09a706d2f61d7c7c80d80e", size = 4451099, upload-time = "2026-08-03T22:32:54.114Z" },
{ url = "https://files.pythonhosted.org/packages/67/57/0b88af1f194ab6c9c650547d9cc06bfeaab836ae4dcdb331676bfb8be95a/hf_xet-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:35cec30d75c6f9eb9c16a77cef68e85a103b72e24d4b473714ec9ff06428bab9", size = 4664712, upload-time = "2026-08-03T22:32:55.547Z" },
{ url = "https://files.pythonhosted.org/packages/53/a0/26b717a9d1840e8abf48dcec64b5ed8fbe472671d38ad28d30e147132b33/hf_xet-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5789835d7c6bc9436962853192082374297fb72d7eff7e7762ec25ceb7e25338", size = 4025906, upload-time = "2026-08-03T22:32:57.391Z" },
{ url = "https://files.pythonhosted.org/packages/49/f6/4a9966633c6fef83af997e2cff68ec1963676d412bdfd096df2a93b8e185/hf_xet-1.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:75765820ce4700db3750c94acc8fe27c5fae4c9ec000a0dbac3ca082acf97765", size = 3849221, upload-time = "2026-08-03T22:32:59.123Z" },
{ url = "https://files.pythonhosted.org/packages/a2/50/7afa2c9c787405864fc47a0d1bbc02c62e9101947ed43c1f43899fc7d91d/hf_xet-1.6.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:633dc0cd71d32da58ab8c03ad38e2fac452c15c2b0a2866ebf6ededfe0a5061d", size = 4071729, upload-time = "2026-08-03T22:33:00.721Z" },
{ url = "https://files.pythonhosted.org/packages/4b/69/55b8dcf636142ae660fec1869fcac14c4da2e8412e14d6eee1523be77e9f/hf_xet-1.6.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:f0906082d9932ae0c0057fa194041c22b4e2cdb46b2592ef3b91f020d62a081a", size = 3876287, upload-time = "2026-08-03T22:33:02.251Z" },
{ url = "https://files.pythonhosted.org/packages/67/4e/a28359bf1c1ecf11eba22123168c138698f7cb576ac678f5a2e16cd5da08/hf_xet-1.6.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d62671bb130879cef0ee4c9ebe47a14af6c66ec53e6d84dc15936e5ffdfac82f", size = 4464663, upload-time = "2026-08-03T22:33:03.802Z" },
{ url = "https://files.pythonhosted.org/packages/9a/69/1f0cbc2fb22ae6082d094f743d1b8945a3f36f6089cb95f42b7ee348cda7/hf_xet-1.6.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0e6e21fa3cdfcdcd76748564bf593870a5e013f47d97cf10aed63aa222cff5b7", size = 4262538, upload-time = "2026-08-03T22:33:05.287Z" },
{ url = "https://files.pythonhosted.org/packages/d1/3a/4f4f2301ade26e404462d3336fa11f7958d914cabbabdd6e03c3c5d5658c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4fc74352a17015bd0ee90038bc9efe38db894cde45f268b6712b04fce8cd0acb", size = 4460520, upload-time = "2026-08-03T22:33:06.81Z" },
{ url = "https://files.pythonhosted.org/packages/ab/5f/311725e2a905534dfee2dcb5b08414f249147f1f12252bfc2bd24caa075c/hf_xet-1.6.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4f71cba6129110c3374a33f919001ff130488fc23553698e34cc1c2a1198c", size = 4675937, upload-time = "2026-08-03T22:33:08.616Z" },
{ url = "https://files.pythonhosted.org/packages/98/b7/8c59a66d15205024662f1d66968136f13893f96df1ddc5087e2e281fc95f/hf_xet-1.6.0-cp38-abi3-win_amd64.whl", hash = "sha256:fb4fadde1b2b70bf4c0c14a6dccbe7194b1c28947fefd5bbe3fed9d940676c3b", size = 4033128, upload-time = "2026-08-03T22:33:10.171Z" },
{ url = "https://files.pythonhosted.org/packages/73/63/ca511b6f802f28cf3489b280fe77475bcca8de85e81a6299d7916b5b5555/hf_xet-1.6.0-cp38-abi3-win_arm64.whl", hash = "sha256:3dc3e35441ba395006af5aaacc40ef2e603c51ef46c3530b9156185f00935ea3", size = 3859359, upload-time = "2026-08-03T22:33:11.725Z" },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
]
[[package]]
name = "huggingface-hub"
version = "1.30.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "filelock" },
{ name = "fsspec" },
{ name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
{ name = "httpx" },
{ name = "packaging" },
{ name = "pyyaml" },
{ name = "tqdm" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/35/97/2eb4abaa5b969ed385066a0496a3823b3ff467fc1082e2202955f1867d60/huggingface_hub-1.30.0.tar.gz", hash = "sha256:e6a6120bc8c8e2723d03648434ee247088cceb55ba7067e7d34d692cad5fdb57", size = 964291, upload-time = "2026-09-03T10:05:14.053Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c3/0e/3e45bbe0dd48f4e56b1d46649d342de853cd1c7e815323472ab62687f153/huggingface_hub-1.30.0-py3-none-any.whl", hash = "sha256:96ae0a8e99a234374a6fe43e989ebd21c04640b91ab2927e7e5773ba1131ca59", size = 796795, upload-time = "2026-09-03T10:05:12.21Z" },
]
[[package]]
name = "identify"
version = "2.6.19"
@@ -2120,6 +2293,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/67/dbcb4928a9d3269f32582ad590b25edc80ff862f52a0854a07688e2c9cf4/sherpa_onnx-1.13.7-cp313-cp313-win32.whl", hash = "sha256:68183b11eda2a866231b9e9aaa35b77222918edef5848a4ff6add6d89f523f6d", size = 1963507, upload-time = "2026-09-01T06:32:41.708Z" },
{ url = "https://files.pythonhosted.org/packages/e9/cf/29c88ff25004b8ab6e6f93cd3a27cc259ab7dcb82b6a842e166b5ca85311/sherpa_onnx-1.13.7-cp313-cp313-win_amd64.whl", hash = "sha256:9fb46a9b0844b2636b099dfa5ef26513fa0b91df28cd710a97d1cda2c9344fec", size = 2282759, upload-time = "2026-09-01T06:43:39.262Z" },
{ url = "https://files.pythonhosted.org/packages/ef/e8/20ced5f876271bbaf2e3bbe2a884265a4e31e382433ec6a3975e8ef0d082/sherpa_onnx-1.13.7-cp313-cp313-win_arm64.whl", hash = "sha256:c2e9c56d453bc804592d65834504e376a4fa90488adba042becf5213cf226328", size = 2241664, upload-time = "2026-09-01T05:56:22.598Z" },
{ url = "https://files.pythonhosted.org/packages/28/45/33e7fa218836035bf7ceda9035a3aaf1b34c5e31241015a72d970eea512e/sherpa_onnx-1.13.7-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:2053956c4575b6ce4621f54ff0af6fae595d7df10691032c79d271b089988ff5", size = 2355187, upload-time = "2026-09-04T14:54:04.717Z" },
{ url = "https://files.pythonhosted.org/packages/9f/23/dfc6a40410c7db7861da7f5a46cbd88054260c8d65e706b12a168e08f689/sherpa_onnx-1.13.7-cp314-cp314-android_24_armeabi_v7a.whl", hash = "sha256:70eecfee42f9104911be6af6573fa8eb9e5e9f658ca04454b5dc24ede1c99f2a", size = 2424902, upload-time = "2026-09-05T22:26:27.069Z" },
{ url = "https://files.pythonhosted.org/packages/ce/32/f2fb82f360e7d53ef5bdb92a1d245e387cf91695ca903cae994c5b364d9d/sherpa_onnx-1.13.7-cp314-cp314-android_24_x86.whl", hash = "sha256:81fea366ccc88074e15b9b7710723f0266cdd13f53b8bff1e5e8b5f95582c00a", size = 2516683, upload-time = "2026-09-05T22:32:46.817Z" },
{ url = "https://files.pythonhosted.org/packages/74/17/b99ccbb0fdb4cb1e4dedef03ec4b8ecbbd909b5d687f7407105dd28aabf7/sherpa_onnx-1.13.7-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26293b1db9a2772f69bd286cfe37551d0bd0437c1f1382e98cfe7d0c4435d331", size = 2577480, upload-time = "2026-09-05T16:43:39.794Z" },
{ url = "https://files.pythonhosted.org/packages/0e/03/c9b41e31bb4ccb64b31618eca1ab857331a9a6285a3ae31697b348556cd3/sherpa_onnx-1.13.7-cp314-cp314-linux_armv7l.whl", hash = "sha256:026917d88fde36d5d86661e4a61c968dd741f43a96773ad742da13bffd030471", size = 12002502, upload-time = "2026-09-01T07:18:23.944Z" },
{ url = "https://files.pythonhosted.org/packages/80/22/e98d505f561c4cf354e5d411d78f6037f23a311fd782836b741f3061a3a4/sherpa_onnx-1.13.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:aa04b2ecf7563f86e8a34e3e2666fc0b480618ccca11c6d629dcb31323c1d041", size = 4446936, upload-time = "2026-09-01T07:03:40.145Z" },
{ url = "https://files.pythonhosted.org/packages/47/8a/f7596357d11117c31b2c044a65669e67225d4b0436ebe8df05f8e6835386/sherpa_onnx-1.13.7-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3f58d2c3ece36b908cfb070ebe1b23c7bfaadd78a0ce205b412349c53375a4dd", size = 2343524, upload-time = "2026-09-01T06:20:31.814Z" },
@@ -2373,6 +2550,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" },
]
[[package]]
name = "tokenizers"
version = "0.23.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "huggingface-hub" },
]
sdist = { url = "https://files.pythonhosted.org/packages/18/1e/bc6587c5ab643b2e17776cace9070a2ae73549c86bffac9934a600bf3c31/tokenizers-0.23.2.tar.gz", hash = "sha256:7f0f085686b9de0d0079e6f874ae053600db64c5d13049e0bbc0119926d25aac", size = 385745, upload-time = "2026-09-03T08:55:42.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/ed/8a443528baa6fac8dfe8c3b75b038c63ac92bb539bcabe311e227c718173/tokenizers-0.23.2-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:85a9a357a3764aecc904ee76bdaf8cf1ad8e5a67a1b929a487c4a39b49ed0e90", size = 3148852, upload-time = "2026-09-03T08:55:30.874Z" },
{ url = "https://files.pythonhosted.org/packages/67/49/22da045a91732384d3a3771816bf188dc5a1f702c32e635afa7c679c0bef/tokenizers-0.23.2-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:986670e43691469dcee610ea0f846f91a8f84e91fc6f7a48d4c064414c0ec2bf", size = 3101593, upload-time = "2026-09-03T08:55:28.587Z" },
{ url = "https://files.pythonhosted.org/packages/2e/4d/8f569ed49372a3ed8e57099bd515055fd48d7c95912c4307cda6973c2168/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a37039b5dfc4af84eb3ef0a92f4307e28936c8f9adccba2629d36f652e9bf7a2", size = 3516830, upload-time = "2026-09-03T08:55:14.741Z" },
{ url = "https://files.pythonhosted.org/packages/2a/de/e2f14c8919d5bf51874051d00d6c7b7e0e8bde6c6a2dbeddda7f642896ff/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b7e37ba198f24150f523e1242e83c4970de4a525480586be5dcc24d9add32c5", size = 3407975, upload-time = "2026-09-03T08:55:16.842Z" },
{ url = "https://files.pythonhosted.org/packages/c5/bd/93c69152d02ef06ce47aed8b2bf4952dcf733c935a62791873932b2934d9/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43e4f2071e3cc8d5d86421c874aebc82659bb51a68bcdef5a0da75ee89511ccb", size = 3748165, upload-time = "2026-09-03T08:55:24.769Z" },
{ url = "https://files.pythonhosted.org/packages/2d/b7/56b84b80bc96942bba8eb23751a9e8a1fce4faaf4390425e7083f721c98c/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:325fee2e0418a9dc6c9ecf736a5f5f0db7875183ace9549ae339da76f7a1fbb7", size = 4024165, upload-time = "2026-09-03T08:55:18.806Z" },
{ url = "https://files.pythonhosted.org/packages/9b/8a/0175e216f005c2fe08238292663aa41e4c802b216e71047a69a0e9fc6fa3/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:950d7c9426fa72406a0ffeacdbc0bb9985f5db20eb8b263f29c79aaf83105703", size = 3591899, upload-time = "2026-09-03T08:55:22.752Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ca/ca6b93c7820df123b2662a9469e8facc826ccc94e98fdd0d615f6431e73a/tokenizers-0.23.2-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41c2f84d172449b4dadb9cdc508e3e364076613c35b16e76ecfe47a60d1e3305", size = 3386843, upload-time = "2026-09-03T08:55:26.584Z" },
{ url = "https://files.pythonhosted.org/packages/e9/a4/4f9106d317b14a80aefea9f0e3a8d07ef25f856a7607eb7f5ab894281fcb/tokenizers-0.23.2-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:12f0835dc2ee694746a76adf7b1567d4346a4a502ebe93fb1f5f80ea49799b78", size = 3577314, upload-time = "2026-09-03T08:55:20.825Z" },
{ url = "https://files.pythonhosted.org/packages/8d/6a/1552b70fb0d9ab074fd3fc961435d01364e79c9058481822c3af6e8d402c/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:eb2f9c8a24da020ea8c11a01a19c1c2547912d92121ae4a01cfbca46125dee40", size = 9967367, upload-time = "2026-09-03T08:55:33.188Z" },
{ url = "https://files.pythonhosted.org/packages/06/01/3ccb3a956c7528b2507b8a9714155c4baf86af593039db6ea375dd0c96c3/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f486f402f6f9abee5bb032553736813af0c710a86b2e0ca592634c55cea1f835", size = 9811886, upload-time = "2026-09-03T08:55:35.642Z" },
{ url = "https://files.pythonhosted.org/packages/fa/73/7038e612d48bda1599457f712f6bd3854eae1a9dc9c13aa47f835349db48/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:bef235815a067b2648caf6dcc7a71091b0b0fff9ee8057f6451eb9335fae52ef", size = 10146224, upload-time = "2026-09-03T08:55:38.391Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d8/8e9e4e0b287a338d8f88976729628c9d22e8a54cfaf9777018a7f7cb58a0/tokenizers-0.23.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5c56bda1511921587789163e524d196ed8284174ac23abd7685d5ea8da6c4718", size = 10256304, upload-time = "2026-09-03T08:55:40.977Z" },
{ url = "https://files.pythonhosted.org/packages/f3/1f/c79a01f671a49728ebb0b61f7ff9ea45663b66cab40bc0858e9859b25c16/tokenizers-0.23.2-cp310-abi3-win32.whl", hash = "sha256:debf978920d93ba9c219bd67cc4bbfaf912c9039e41e7a28b91ec15e3728c95a", size = 2592809, upload-time = "2026-09-03T08:55:48.02Z" },
{ url = "https://files.pythonhosted.org/packages/db/f7/0a69ac6b82dbccf3f71add938a161c497952749294b8dd6dfe03a819dc40/tokenizers-0.23.2-cp310-abi3-win_amd64.whl", hash = "sha256:2e96f5699d5249c9c64aa8412e044f727aae3a4098cf830f9901ec1afc361cde", size = 2863236, upload-time = "2026-09-03T08:55:46.193Z" },
{ url = "https://files.pythonhosted.org/packages/d7/b0/dee84cb44175be1b4c35bd2f770727494e78f0bb38e571a623ade94dbebb/tokenizers-0.23.2-cp310-abi3-win_arm64.whl", hash = "sha256:e49c394456dd9985787fec76132438ba3fb8911f857b1bf3d40119f9292d41aa", size = 2729352, upload-time = "2026-09-03T08:55:44.345Z" },
]
[[package]]
name = "tomli"
version = "2.4.1"
@@ -2592,6 +2796,9 @@ docs = [
{ name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
{ name = "sphinx-rtd-theme" },
]
faster-whisper = [
{ name = "faster-whisper" },
]
parakeet = [
{ name = "sherpa-onnx" },
]
@@ -2617,6 +2824,7 @@ lint = [
[package.metadata]
requires-dist = [
{ name = "evdev", marker = "sys_platform == 'linux'", specifier = ">=1.6.0,<2" },
{ name = "faster-whisper", marker = "extra == 'faster-whisper'", specifier = ">=1.0.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.0.0" },
{ name = "numpy", specifier = ">=1.22.0,<3" },
{ name = "onnxruntime", marker = "extra == 'vad'", specifier = ">=1.16.0" },
@@ -2639,7 +2847,7 @@ requires-dist = [
{ name = "torchaudio", marker = "extra == 'whisper'", specifier = ">=2.0.0" },
{ name = "vosk", marker = "extra == 'vosk'", specifier = ">=0.3.45" },
]
provides-extras = ["vosk", "whisper", "parakeet", "vad", "dev", "docs"]
provides-extras = ["vosk", "whisper", "parakeet", "faster-whisper", "vad", "dev", "docs"]
[package.metadata.requires-dev]
lint = [