fix(languages): map English (India) to Whisper code en (#617)

* fix(languages): map en-in to Whisper code en

English (India) used the catalog id as the Whisper language argument,
which is invalid. Resolve Whisper/whisper.cpp/remote_api codes via the
SUPPORTED_LANGUAGES whisper field so en-us and en-in both become en.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

* test(languages): cover whisper language resolver fallbacks

Exercise unknown catalog ids and legacy en-us fallback paths so
codecov/patch covers resolve_whisper_language fully.

Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
This commit is contained in:
Jatin K Malik
2026-07-28 21:31:02 -07:00
committed by GitHub
co-authored by Jatin K Malik Cursor Agent
parent 28bed4c1fd
commit cc69c7ebda
3 changed files with 63 additions and 16 deletions
@@ -21,13 +21,31 @@ 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.paths import models_dir
from ..utils.vosk_model_info import VOSK_MODEL_INFO
from ..utils.vosk_model_info import SUPPORTED_LANGUAGES, VOSK_MODEL_INFO
from ..utils.whispercpp_model_info import WHISPERCPP_MODEL_INFO, get_model_path, is_model_downloaded
from ..version import __version__
from .command_processor import CommandProcessor
from .silero_vad import SILERO_CHUNK_SIZE, load_silero_vad
def resolve_whisper_language(language: str) -> Optional[str]:
"""Map a catalog language id to a Whisper / whisper.cpp language code.
Catalog keys like ``en-us`` / ``en-in`` are not valid Whisper codes; the
``whisper`` field on ``SUPPORTED_LANGUAGES`` holds the ISO-style code
(``en``). ``auto`` resolves to ``None`` for model auto-detect.
"""
if language == "auto":
return None
info = SUPPORTED_LANGUAGES.get(language)
if info is not None and "whisper" in info:
return info["whisper"]
# Legacy fallback for codes not present in the catalog.
if language == "en-us":
return "en"
return language
# ALSA error handler to suppress warnings during PyAudio initialization
def _setup_alsa_error_handler():
"""Set up an error handler to suppress ALSA warnings."""
@@ -1137,11 +1155,7 @@ class SpeechRecognitionManager:
import torch
use_fp16 = self.model.device != torch.device("cpu")
lang = self.language
if self.language == "en-us":
lang = "en"
elif self.language == "auto":
lang = None # Auto-detect
lang = resolve_whisper_language(self.language)
# Transcribe with Whisper (handles variable length audio automatically)
result = self.model.transcribe(
@@ -1488,11 +1502,7 @@ class SpeechRecognitionManager:
)
# Prepare language parameter
lang = self.language
if self.language == "en-us":
lang = "en"
elif self.language == "auto":
lang = None # Auto-detect
lang = resolve_whisper_language(self.language)
logger.debug(f"whisper.cpp using language: {lang or 'auto-detect'}")
@@ -1625,11 +1635,7 @@ class SpeechRecognitionManager:
)
# Prepare language parameters
lang = self.language
if lang == "en-us":
lang = "en"
elif lang == "auto":
lang = None
lang = resolve_whisper_language(self.language)
# Prepare HTTP request headers
headers = {}
@@ -366,6 +366,18 @@ class TestRemoteAPITranscription(unittest.TestCase):
# Should have been called with "en" (converted from "en-us")
mock_server.assert_called_once_with(ANY, "en", ANY, ANY)
def test_transcribe_with_language_en_in_conversion(self):
"""English (India) catalog id must map to Whisper code 'en'."""
from unittest.mock import patch
with patch.object(self.manager, "_try_whispercpp_server_api") as mock_server:
mock_server.return_value = "test"
self.manager.language = "en-in"
self.manager._transcribe_with_remote_api([b"audio"], self.manager._http_session)
mock_server.assert_called_once_with(ANY, "en", ANY, ANY)
def test_transcribe_with_language_auto(self):
"""Test with auto language detection."""
from unittest.mock import patch
+29
View File
@@ -305,6 +305,35 @@ class TestSpeechRecognition(unittest.TestCase):
self.assertIsNone(hu["vosk"])
self.assertNotIn("hu", VOSK_MODEL_INFO["small"]["languages"])
def test_resolve_whisper_language_maps_english_variants(self):
"""Catalog ids like en-us/en-in must become Whisper's 'en' code."""
from unittest.mock import patch
from vocalinux.speech_recognition.recognition_manager import resolve_whisper_language
self.assertEqual(resolve_whisper_language("en-us"), "en")
self.assertEqual(resolve_whisper_language("en-in"), "en")
self.assertEqual(resolve_whisper_language("hu"), "hu")
self.assertEqual(resolve_whisper_language("ja"), "ja")
self.assertIsNone(resolve_whisper_language("auto"))
# Unknown catalog ids pass through unchanged.
self.assertEqual(resolve_whisper_language("zz-unknown"), "zz-unknown")
# Legacy fallback when a code is missing from the catalog entirely.
with patch(
"vocalinux.speech_recognition.recognition_manager.SUPPORTED_LANGUAGES",
{},
):
self.assertEqual(resolve_whisper_language("en-us"), "en")
self.assertEqual(resolve_whisper_language("fr"), "fr")
# Entry present but without a whisper field still falls through.
with patch(
"vocalinux.speech_recognition.recognition_manager.SUPPORTED_LANGUAGES",
{"legacy": {"name": "Legacy", "vosk": None}},
):
self.assertEqual(resolve_whisper_language("legacy"), "legacy")
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")