fix: persist model selection per-engine when switching between VOSK and Whisper (#64)
- Add vosk_model_size and whisper_model_size to config to track model selection independently for each engine - Update config manager with get_model_size_for_engine() and set_model_size_for_engine() methods for per-engine model tracking - Add automatic migration for existing configs without per-engine model sizes - Update settings dialog to use engine-specific model sizes when populating model options - Update install.sh to include new per-engine model fields in default config - Fix: Previously, switching from VOSK to Whisper would incorrectly try to download the recommended model instead of the previously used model Fixes the issue where switching back to Whisper after using VOSK would start downloading the medium model instead of remembering the tiny model that was configured during installation.
This commit is contained in:
@@ -670,6 +670,8 @@ install_python_package() {
|
||||
"speech_recognition": {
|
||||
"engine": "vosk",
|
||||
"model_size": "small",
|
||||
"vosk_model_size": "small",
|
||||
"whisper_model_size": "tiny",
|
||||
"vad_sensitivity": 3,
|
||||
"silence_timeout": 2.0
|
||||
},
|
||||
|
||||
@@ -19,7 +19,9 @@ CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
|
||||
DEFAULT_CONFIG = {
|
||||
"speech_recognition": { # Changed section name
|
||||
"engine": "whisper", # "vosk" or "whisper" - whisper is default for better accuracy
|
||||
"model_size": "tiny", # "tiny", "small", "medium", or "large"
|
||||
"model_size": "tiny", # Current model size (for backward compatibility)
|
||||
"vosk_model_size": "small", # Default model for VOSK engine
|
||||
"whisper_model_size": "tiny", # Default model for Whisper engine
|
||||
"vad_sensitivity": 3, # Voice Activity Detection sensitivity (1-5)
|
||||
"silence_timeout": 2.0, # Seconds of silence before stopping
|
||||
},
|
||||
@@ -51,7 +53,9 @@ class ConfigManager:
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the configuration manager."""
|
||||
self.config = DEFAULT_CONFIG.copy()
|
||||
import copy
|
||||
|
||||
self.config = copy.deepcopy(DEFAULT_CONFIG)
|
||||
self._ensure_config_dir()
|
||||
self.load_config()
|
||||
|
||||
@@ -73,13 +77,53 @@ class ConfigManager:
|
||||
with open(CONFIG_FILE, "r") as f:
|
||||
user_config = json.load(f)
|
||||
|
||||
# Check if migration is needed BEFORE merging with defaults
|
||||
needs_migration = self._check_needs_migration(user_config)
|
||||
|
||||
# Update the default config with user settings
|
||||
self._update_dict_recursive(self.config, user_config)
|
||||
logger.info(f"Loaded configuration from {CONFIG_FILE}")
|
||||
|
||||
# Migrate old config format if needed
|
||||
if needs_migration:
|
||||
self._migrate_config(user_config)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load config: {e}")
|
||||
|
||||
def _check_needs_migration(self, user_config: Dict) -> bool:
|
||||
"""Check if the user config needs migration to add per-engine model sizes."""
|
||||
sr_config = user_config.get("speech_recognition", {})
|
||||
# Need migration if we have model_size but not the per-engine keys
|
||||
return "model_size" in sr_config and (
|
||||
"vosk_model_size" not in sr_config or "whisper_model_size" not in sr_config
|
||||
)
|
||||
|
||||
def _migrate_config(self, user_config: Dict):
|
||||
"""Migrate old config formats to the current format."""
|
||||
sr_config = self.config.get("speech_recognition", {})
|
||||
user_sr_config = user_config.get("speech_recognition", {})
|
||||
|
||||
# Get the current engine and model from the user's original config
|
||||
current_engine = user_sr_config.get("engine", "vosk")
|
||||
current_model = user_sr_config.get("model_size", "small")
|
||||
|
||||
# Set the per-engine model sizes based on the user's original config
|
||||
if "vosk_model_size" not in user_sr_config:
|
||||
# If current engine is vosk, use the current model; otherwise use default
|
||||
sr_config["vosk_model_size"] = current_model if current_engine == "vosk" else "small"
|
||||
logger.info(f"Migrated vosk_model_size to: {sr_config['vosk_model_size']}")
|
||||
|
||||
if "whisper_model_size" not in user_sr_config:
|
||||
# If current engine is whisper, use the current model; otherwise use default
|
||||
sr_config["whisper_model_size"] = (
|
||||
current_model if current_engine == "whisper" else "tiny"
|
||||
)
|
||||
logger.info(f"Migrated whisper_model_size to: {sr_config['whisper_model_size']}")
|
||||
|
||||
self.save_config()
|
||||
logger.info("Config migrated to new per-engine model format")
|
||||
|
||||
def save_config(self):
|
||||
"""Save the current configuration to the config file."""
|
||||
try:
|
||||
@@ -143,12 +187,53 @@ class ConfigManager:
|
||||
"""Get the entire configuration dictionary."""
|
||||
return self.config
|
||||
|
||||
def get_model_size_for_engine(self, engine: str) -> str:
|
||||
"""Get the saved model size for a specific engine.
|
||||
|
||||
Args:
|
||||
engine: The engine name ("vosk" or "whisper")
|
||||
|
||||
Returns:
|
||||
The model size for the engine, or the default if not found
|
||||
"""
|
||||
sr_config = self.config.get("speech_recognition", {})
|
||||
|
||||
# Try engine-specific model size first
|
||||
engine_key = f"{engine.lower()}_model_size"
|
||||
if engine_key in sr_config:
|
||||
return sr_config[engine_key]
|
||||
|
||||
# Fall back to generic model_size for backward compatibility
|
||||
return sr_config.get("model_size", "small" if engine == "vosk" else "tiny")
|
||||
|
||||
def set_model_size_for_engine(self, engine: str, model_size: str):
|
||||
"""Set the model size for a specific engine.
|
||||
|
||||
Args:
|
||||
engine: The engine name ("vosk" or "whisper")
|
||||
model_size: The model size to save
|
||||
"""
|
||||
if "speech_recognition" not in self.config:
|
||||
self.config["speech_recognition"] = {}
|
||||
|
||||
engine_key = f"{engine.lower()}_model_size"
|
||||
self.config["speech_recognition"][engine_key] = model_size
|
||||
# Also update the generic model_size for backward compatibility
|
||||
self.config["speech_recognition"]["model_size"] = model_size
|
||||
logger.info(f"Set {engine} model size to: {model_size}")
|
||||
|
||||
def update_speech_recognition_settings(self, settings: Dict[str, Any]):
|
||||
"""Update multiple speech recognition settings at once."""
|
||||
if "speech_recognition" not in self.config:
|
||||
self.config["speech_recognition"] = {}
|
||||
|
||||
# Only update keys present in the provided settings dict
|
||||
# Handle engine-specific model size updates
|
||||
if "engine" in settings and "model_size" in settings:
|
||||
engine = settings["engine"]
|
||||
model_size = settings["model_size"]
|
||||
self.set_model_size_for_engine(engine, model_size)
|
||||
|
||||
# Update all other keys present in the provided settings dict
|
||||
for key, value in settings.items():
|
||||
self.config["speech_recognition"][key] = value
|
||||
logger.info(f"Updated speech recognition settings: {settings}")
|
||||
|
||||
@@ -21,9 +21,7 @@ from ..common_types import RecognitionState # noqa: E402
|
||||
|
||||
# Avoid circular imports for type checking
|
||||
if TYPE_CHECKING:
|
||||
from ..speech_recognition.recognition_manager import ( # noqa: E402
|
||||
SpeechRecognitionManager,
|
||||
)
|
||||
from ..speech_recognition.recognition_manager import SpeechRecognitionManager # noqa: E402
|
||||
from .config_manager import ConfigManager # noqa: E402
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -586,7 +584,8 @@ class SettingsDialog(Gtk.Dialog):
|
||||
# Use .get with defaults for robustness
|
||||
sr_settings = settings.get("speech_recognition", {})
|
||||
engine = sr_settings.get("engine", "vosk")
|
||||
model_size = sr_settings.get("model_size", "small")
|
||||
# Get model size for the specific engine
|
||||
model_size = self.config_manager.get_model_size_for_engine(engine)
|
||||
vad_sensitivity = sr_settings.get("vad_sensitivity", 3)
|
||||
silence_timeout = sr_settings.get("silence_timeout", 2.0)
|
||||
|
||||
@@ -616,12 +615,9 @@ class SettingsDialog(Gtk.Dialog):
|
||||
engine = engine_text.lower()
|
||||
logger.info(f"Populating model options for engine: {engine}")
|
||||
|
||||
# Get recommended model based on engine
|
||||
recommended_model = None
|
||||
if engine == "whisper":
|
||||
recommended_model, _ = _get_recommended_whisper_model()
|
||||
elif engine == "vosk":
|
||||
recommended_model, _ = _get_recommended_vosk_model()
|
||||
# Get the saved model size for THIS specific engine (not the generic one)
|
||||
saved_model_for_engine = self.config_manager.get_model_size_for_engine(engine)
|
||||
logger.info(f"Saved model for {engine}: {saved_model_for_engine}")
|
||||
|
||||
# Track which models are downloaded and find smallest downloaded
|
||||
downloaded_models = []
|
||||
@@ -635,10 +631,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
info = WHISPER_MODEL_INFO[size]
|
||||
is_downloaded = _is_whisper_model_downloaded(size)
|
||||
status = "✓" if is_downloaded else "↓"
|
||||
rec = " ★" if size == recommended_model else ""
|
||||
display_text = (
|
||||
f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}{rec}"
|
||||
)
|
||||
display_text = f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}"
|
||||
if is_downloaded:
|
||||
downloaded_models.append(size)
|
||||
if smallest_model is None:
|
||||
@@ -647,10 +640,7 @@ class SettingsDialog(Gtk.Dialog):
|
||||
info = VOSK_MODEL_INFO[size]
|
||||
is_downloaded = _is_vosk_model_downloaded(size)
|
||||
status = "✓" if is_downloaded else "↓"
|
||||
rec = " ★" if size == recommended_model else ""
|
||||
display_text = (
|
||||
f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}{rec}"
|
||||
)
|
||||
display_text = f"{size.capitalize()} ({_format_size(info['size_mb'])}) {status}"
|
||||
if is_downloaded:
|
||||
downloaded_models.append(size)
|
||||
if smallest_model is None:
|
||||
@@ -661,19 +651,29 @@ class SettingsDialog(Gtk.Dialog):
|
||||
self.model_combo.append(size.capitalize(), display_text)
|
||||
|
||||
# Determine which model to select:
|
||||
# 1. If saved model is downloaded, use it
|
||||
# 2. Else if any model is downloaded, use the first (smallest) downloaded
|
||||
# 3. Else use the smallest model (but don't auto-download)
|
||||
saved_model = self.current_model_size.lower()
|
||||
if saved_model in downloaded_models:
|
||||
# 1. If saved model for this engine is downloaded, use it
|
||||
# 2. Else if saved model for this engine exists (even if not downloaded), use it
|
||||
# (user will be prompted to download when applying)
|
||||
# 3. Else if any model is downloaded, use the smallest downloaded
|
||||
# 4. Else use the smallest model for this engine
|
||||
saved_model = saved_model_for_engine.lower()
|
||||
|
||||
# Check if the saved model is valid for this engine
|
||||
valid_models = [m.lower() for m in ENGINE_MODELS.get(engine, [])]
|
||||
|
||||
if saved_model in valid_models:
|
||||
# Use the saved model for this engine (whether downloaded or not)
|
||||
model_to_set = saved_model.capitalize()
|
||||
elif downloaded_models:
|
||||
# Saved model isn't valid for this engine, use smallest downloaded
|
||||
model_to_set = downloaded_models[0].capitalize()
|
||||
else:
|
||||
# No downloaded models, use the smallest model for this engine
|
||||
model_to_set = smallest_model.capitalize() if smallest_model else "Small"
|
||||
|
||||
logger.info(
|
||||
f"Setting active model to: {model_to_set} (saved={saved_model}, downloaded={downloaded_models})"
|
||||
f"Setting active model to: {model_to_set} (saved_for_engine={saved_model}, "
|
||||
f"valid={saved_model in valid_models}, downloaded={downloaded_models})"
|
||||
)
|
||||
|
||||
# Try to set by ID
|
||||
|
||||
@@ -200,3 +200,76 @@ class TestConfigManager(unittest.TestCase):
|
||||
self.assertEqual(target["a"]["e"], 20) # Added
|
||||
self.assertEqual(target["d"], 3) # Unchanged
|
||||
self.assertEqual(target["f"], 30) # Added
|
||||
|
||||
def test_get_model_size_for_engine(self):
|
||||
"""Test getting model size for a specific engine."""
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# Test default values
|
||||
self.assertEqual(config_manager.get_model_size_for_engine("vosk"), "small")
|
||||
self.assertEqual(config_manager.get_model_size_for_engine("whisper"), "tiny")
|
||||
|
||||
# Set specific model sizes
|
||||
config_manager.set_model_size_for_engine("vosk", "medium")
|
||||
config_manager.set_model_size_for_engine("whisper", "small")
|
||||
|
||||
# Verify they are returned correctly
|
||||
self.assertEqual(config_manager.get_model_size_for_engine("vosk"), "medium")
|
||||
self.assertEqual(config_manager.get_model_size_for_engine("whisper"), "small")
|
||||
|
||||
def test_set_model_size_for_engine(self):
|
||||
"""Test setting model size for a specific engine."""
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# Set model size for vosk
|
||||
config_manager.set_model_size_for_engine("vosk", "large")
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["vosk_model_size"], "large")
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["model_size"], "large")
|
||||
|
||||
# Set model size for whisper
|
||||
config_manager.set_model_size_for_engine("whisper", "medium")
|
||||
self.assertEqual(
|
||||
config_manager.config["speech_recognition"]["whisper_model_size"], "medium"
|
||||
)
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["model_size"], "medium")
|
||||
|
||||
def test_migrate_old_config(self):
|
||||
"""Test migration of old config format without per-engine model sizes."""
|
||||
# Create an old-style config file without per-engine model sizes
|
||||
old_config = {
|
||||
"speech_recognition": {
|
||||
"engine": "vosk",
|
||||
"model_size": "medium",
|
||||
"vad_sensitivity": 3,
|
||||
"silence_timeout": 2.0,
|
||||
}
|
||||
}
|
||||
|
||||
with open(self.temp_config_file, "w") as f:
|
||||
json.dump(old_config, f)
|
||||
|
||||
# Load the config (should trigger migration)
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# Verify migration added per-engine model sizes
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["vosk_model_size"], "medium")
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["whisper_model_size"], "tiny")
|
||||
|
||||
def test_update_speech_recognition_settings_per_engine(self):
|
||||
"""Test that update_speech_recognition_settings saves per-engine model sizes."""
|
||||
config_manager = ConfigManager()
|
||||
|
||||
# Update settings for vosk
|
||||
config_manager.update_speech_recognition_settings({"engine": "vosk", "model_size": "large"})
|
||||
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["vosk_model_size"], "large")
|
||||
|
||||
# Update settings for whisper
|
||||
config_manager.update_speech_recognition_settings(
|
||||
{"engine": "whisper", "model_size": "small"}
|
||||
)
|
||||
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["whisper_model_size"], "small")
|
||||
|
||||
# Verify the vosk setting wasn't changed
|
||||
self.assertEqual(config_manager.config["speech_recognition"]["vosk_model_size"], "large")
|
||||
|
||||
Reference in New Issue
Block a user