fix(vosk): add missing Italian and English-India entries to medium/large model tables (#551)

Fixes #550.

Root cause: VOSK_MODEL_INFO["medium"]["languages"] and
VOSK_MODEL_INFO["large"]["languages"] in vosk_model_info.py were missing
entries for "it" (Italian) and "en-in" (English-India), even though both
languages are present in the "small" table and VOSK actually publishes
bigger models for them (vosk-model-it-0.22, vosk-model-en-in-0.5 at
alphacephei.com/vosk/models).

When a user selected Italian + the medium (or large) VOSK model,
_init_vosk() built vosk_model_map["medium"] = None (via .get() on a
missing key), and _get_vosk_model_path() then called
os.path.join(MODELS_DIR, None), crashing with:

    join() argument must be str, bytes, or os.PathLike object, not 'NoneType'

before any download was even attempted. This matches the exact debug log
in the issue and the settings-dialog download flow the reporter exercised.

Fix:
- Add the missing "it"/"en-in" entries to the medium/large tables so all
  10 supported languages now resolve consistently across all three sizes.
- Add a guard in _get_vosk_model_path() that raises a clear ValueError
  ("No VOSK '<size>' model available for language '<lang>'...") instead of
  crashing deep inside os.path.join, so any future data gap fails loudly
  and helpfully rather than with a cryptic TypeError.
This commit is contained in:
Anay Garodia
2026-07-19 22:53:07 -07:00
committed by GitHub
parent 830d24577a
commit fbcd28d577
3 changed files with 68 additions and 0 deletions
@@ -1824,6 +1824,13 @@ class SpeechRecognitionManager:
"""Get the path to the VOSK model based on the selected size and language."""
model_name = self.vosk_model_map.get(self.model_size, self.vosk_model_map["small"])
if not model_name:
vosk_language = "en-us" if self.language == "auto" else self.language
raise ValueError(
f"No VOSK '{self.model_size}' model available for language '{vosk_language}'. "
"Try the 'small' model size or a different language."
)
# First, check user's local models directory
user_model_path = os.path.join(MODELS_DIR, model_name)
if os.path.exists(user_model_path):
+4
View File
@@ -78,8 +78,10 @@ VOSK_MODEL_INFO = {
"desc": "Balanced accuracy/speed",
"languages": {
"en-us": "vosk-model-en-us-0.22",
"en-in": "vosk-model-en-in-0.5",
"fr": "vosk-model-fr-0.22",
"de": "vosk-model-de-0.21",
"it": "vosk-model-it-0.22",
"ru": "vosk-model-ru-0.22",
"hi": "vosk-model-hi-0.22",
"es": "vosk-model-es-0.42",
@@ -92,8 +94,10 @@ VOSK_MODEL_INFO = {
"desc": "Same as medium (best available)",
"languages": {
"en-us": "vosk-model-en-us-0.22",
"en-in": "vosk-model-en-in-0.5",
"fr": "vosk-model-fr-0.22",
"de": "vosk-model-de-0.21",
"it": "vosk-model-it-0.22",
"ru": "vosk-model-ru-0.22",
"hi": "vosk-model-hi-0.22",
"es": "vosk-model-es-0.42",
+57
View File
@@ -69,6 +69,7 @@ from conftest import mock_audio_feedback # noqa: E402
# Update import paths to use the new package structure
from vocalinux.common_types import RecognitionState # noqa: E402
from vocalinux.speech_recognition.recognition_manager import SpeechRecognitionManager # noqa: E402
from vocalinux.utils.vosk_model_info import VOSK_MODEL_INFO # noqa: E402
class TestSpeechRecognition(unittest.TestCase):
@@ -251,6 +252,62 @@ class TestSpeechRecognition(unittest.TestCase):
# Verify the large model path is constructed correctly
mock_get_path.assert_called_with()
def test_vosk_medium_and_large_cover_all_small_languages(self):
"""Regression test for issue #550.
The 'medium' and 'large' VOSK model tables must map every language
that 'small' supports. Italian ('it') and English-India ('en-in')
were previously missing from 'medium'/'large', which caused
vosk_model_map[size] to resolve to None and crash later with
"join() argument must be str, bytes, or os.PathLike object, not
'NoneType'" instead of downloading the (existing) big model.
"""
small_languages = set(VOSK_MODEL_INFO["small"]["languages"])
for size in ("medium", "large"):
size_languages = set(VOSK_MODEL_INFO[size]["languages"])
missing = small_languages - size_languages
self.assertEqual(
missing,
set(),
f"VOSK_MODEL_INFO['{size}']['languages'] is missing languages "
f"that 'small' supports: {missing}. Any language present in "
"'small' must also resolve to a model for 'medium'/'large', "
"otherwise initialization crashes with a NoneType path error.",
)
def test_vosk_init_italian_medium_model_resolves_correctly(self):
"""Selecting Italian + the 'medium' VOSK model must not crash (issue #550)."""
manager = SpeechRecognitionManager(engine="vosk", language="it", model_size="medium")
self.assertEqual(
manager.vosk_model_map["medium"],
VOSK_MODEL_INFO["medium"]["languages"]["it"],
)
self.assertIn("vosk-model-it-0.22", manager.vosk_model_path)
def test_get_vosk_model_path_raises_clear_error_for_unmapped_language(self):
"""An unmapped (size, language) combination should raise a clear,
actionable error instead of crashing deep inside os.path.join with a
cryptic TypeError (the actual failure mode reported in issue #550,
before the VOSK_MODEL_INFO data gap was fixed)."""
manager = SpeechRecognitionManager(engine="vosk", language="en-us", model_size="small")
# Simulate a language that has no mapping for the requested size,
# mirroring what happened for "it"/"medium" prior to the data fix.
manager.model_size = "medium"
manager.language = "it"
manager.vosk_model_map = {
"small": "vosk-model-small-it-0.22",
"medium": None,
"large": None,
}
with self.assertRaises(ValueError) as ctx:
manager._get_vosk_model_path()
self.assertIn("medium", str(ctx.exception))
self.assertIn("it", str(ctx.exception))
def test_download_vosk_model(self):
"""Test downloading VOSK model."""
# Make os.path.exists return False to trigger download