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:
+67
-6
@@ -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)"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"])
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user