2 Commits

Author SHA1 Message Date
Storm Dragon
c66a9ba9c2 Problems with voice selection fixed.: 2025-12-02 18:38:06 -05:00
Storm Dragon
2092a3e257 Fixed voice selection. 2025-12-02 18:36:46 -05:00

View File

@@ -48,7 +48,8 @@ class SpeechHelperMixin:
if result.returncode == 0: if result.returncode == 0:
lines = result.stdout.strip().split("\n") lines = result.stdout.strip().split("\n")
self._modules_cache = [ self._modules_cache = [
line.strip() for line in lines[1:] if line.strip() line.strip() for line in lines[1:]
if line.strip() and line.strip().lower() != "dummy"
] ]
self._cache_timestamp = now self._cache_timestamp = now
return self._modules_cache return self._modules_cache
@@ -92,8 +93,13 @@ class SpeechHelperMixin:
voice = self._process_espeak_voice(line) voice = self._process_espeak_voice(line)
if voice: if voice:
voices.append(voice) voices.append(voice)
elif module.lower() == "voxin":
# For Voxin, store voice name with language
voice_data = self._process_voxin_voice(line)
if voice_data:
voices.append(voice_data)
else: else:
# For non-espeak modules, extract first field (voice name) # For other modules, extract first field (voice name)
parts = line.strip().split() parts = line.strip().split()
if parts: if parts:
voices.append(parts[0]) voices.append(parts[0])
@@ -126,6 +132,91 @@ class SpeechHelperMixin:
return (f"{lang_code}+{variant}" return (f"{lang_code}+{variant}"
if variant and variant != "none" else lang_code) if variant and variant != "none" else lang_code)
def _process_voxin_voice(self, voice_line):
"""Process Voxin voice format with language information.
Args:
voice_line (str): Raw line from spd-say -o voxin -L output
Format: NAME LANGUAGE VARIANT
Returns:
str: Voice name with language encoded (e.g., 'daniel-embedded-high|en-GB')
"""
parts = [p for p in voice_line.split() if p]
if len(parts) < 2:
return None
voice_name = parts[0]
language = parts[1]
# Encode language with voice for later extraction
return f"{voice_name}|{language}"
def _select_default_voice(self, voices):
"""Select a sensible default voice from list, preferring user's
language.
Args:
voices (list): List of available voice names
Returns:
str: Selected default voice (matches user language if possible)
"""
if not voices:
return ""
# Get current voice to preserve language preference
current_voice = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "voice"
)
# Get configured language from settings
configured_lang = self.env["runtime"]["SettingsManager"].get_setting(
"speech", "language"
)
# Extract language code from current voice if available
current_lang = None
if current_voice:
# Extract language code (e.g., 'en-gb' from 'en-gb+male')
current_lang = current_voice.split('+')[0].lower()
# Build preference list: current language, configured language, English
preferences = []
if current_lang:
preferences.append(current_lang)
if configured_lang:
preferences.append(configured_lang.lower())
preferences.extend(['en-gb', 'en-us', 'en'])
# Remove duplicates while preserving order
seen = set()
preferences = [x for x in preferences
if not (x in seen or seen.add(x))]
# Try exact matches for preferred languages
for pref in preferences:
for voice in voices:
# Extract language if voice is in "name|lang" format
voice_to_check = voice
if "|" in voice:
_, voice_lang = voice.split("|", 1)
voice_to_check = voice_lang
if voice_to_check.lower() == pref:
return voice
# Try voices starting with preferred language codes
for pref in preferences:
for voice in voices:
# Extract language if voice is in "name|lang" format
voice_to_check = voice
if "|" in voice:
_, voice_lang = voice.split("|", 1)
voice_to_check = voice_lang
if voice_to_check.lower().startswith(pref):
return voice
# Fall back to first available voice
return voices[0]
def invalidate_speech_cache(self): def invalidate_speech_cache(self):
"""Clear cached module and voice data.""" """Clear cached module and voice data."""
self._modules_cache = None self._modules_cache = None
@@ -387,13 +478,43 @@ class QuickMenuManager(SpeechHelperMixin):
"speech", "module", new_module "speech", "module", new_module
) )
# Reset voice to first available for new module # Select sensible default voice for new module
voices = self.get_module_voices(new_module) voices = self.get_module_voices(new_module)
if voices: if voices:
default_voice = self._select_default_voice(voices)
# Parse voice name and language for modules like Voxin
voice_name = default_voice
voice_lang = None
if "|" in default_voice:
voice_name, voice_lang = default_voice.split("|", 1)
self.env["runtime"]["SettingsManager"].set_setting( self.env["runtime"]["SettingsManager"].set_setting(
"speech", "voice", voices[0] "speech", "voice", voice_name
) )
# Apply voice to speech driver immediately
if "SpeechDriver" in self.env["runtime"]:
try:
self.env["runtime"]["SpeechDriver"].set_module(
new_module
)
# Set language first if available
if voice_lang:
self.env["runtime"]["SpeechDriver"].set_language(
voice_lang
)
# Then set voice
self.env["runtime"]["SpeechDriver"].set_voice(
voice_name
)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
(f"QuickMenuManager cycle_speech_module: "
f"Error applying voice: {e}"),
debug.DebugLevel.ERROR
)
# Announce new module # Announce new module
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
new_module, interrupt=True new_module, interrupt=True
@@ -442,12 +563,19 @@ class QuickMenuManager(SpeechHelperMixin):
"speech", "voice" "speech", "voice"
) )
# Find current index # Find current index (handle Voxin voice|language format)
try: current_index = 0
current_index = (voices.index(current_voice) if current_voice:
if current_voice else 0) try:
except ValueError: # Try exact match first
current_index = 0 current_index = voices.index(current_voice)
except ValueError:
# For Voxin, compare just the voice name part
for i, voice in enumerate(voices):
voice_name = voice.split("|")[0] if "|" in voice else voice
if voice_name == current_voice:
current_index = i
break
# Cycle to next/previous # Cycle to next/previous
if direction == "next": if direction == "next":
@@ -457,14 +585,38 @@ class QuickMenuManager(SpeechHelperMixin):
new_voice = voices[new_index] new_voice = voices[new_index]
# Update setting (runtime only) # Parse voice name and language for modules like Voxin
voice_name = new_voice
voice_lang = None
if "|" in new_voice:
# Format: "voicename|language" (e.g., "daniel-embedded-high|en-GB")
voice_name, voice_lang = new_voice.split("|", 1)
# Update setting (runtime only) - store the voice name only
self.env["runtime"]["SettingsManager"].set_setting( self.env["runtime"]["SettingsManager"].set_setting(
"speech", "voice", new_voice "speech", "voice", voice_name
) )
# Announce new voice # Apply voice to speech driver immediately
if "SpeechDriver" in self.env["runtime"]:
try:
# Set language first if available
if voice_lang:
self.env["runtime"]["SpeechDriver"].set_language(
voice_lang
)
# Then set voice
self.env["runtime"]["SpeechDriver"].set_voice(voice_name)
except Exception as e:
self.env["runtime"]["DebugManager"].write_debug_out(
(f"QuickMenuManager cycle_speech_voice: "
f"Error applying voice: {e}"),
debug.DebugLevel.ERROR
)
# Announce new voice (voice name only, not language)
self.env["runtime"]["OutputManager"].present_text( self.env["runtime"]["OutputManager"].present_text(
new_voice, interrupt=True voice_name, interrupt=True
) )
return True return True