Compare commits
2 Commits
d46d8de3ee
...
c66a9ba9c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c66a9ba9c2 | ||
|
|
2092a3e257 |
@@ -48,7 +48,8 @@ class SpeechHelperMixin:
|
||||
if result.returncode == 0:
|
||||
lines = result.stdout.strip().split("\n")
|
||||
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
|
||||
return self._modules_cache
|
||||
@@ -92,8 +93,13 @@ class SpeechHelperMixin:
|
||||
voice = self._process_espeak_voice(line)
|
||||
if 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:
|
||||
# For non-espeak modules, extract first field (voice name)
|
||||
# For other modules, extract first field (voice name)
|
||||
parts = line.strip().split()
|
||||
if parts:
|
||||
voices.append(parts[0])
|
||||
@@ -126,6 +132,91 @@ class SpeechHelperMixin:
|
||||
return (f"{lang_code}+{variant}"
|
||||
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):
|
||||
"""Clear cached module and voice data."""
|
||||
self._modules_cache = None
|
||||
@@ -387,11 +478,41 @@ class QuickMenuManager(SpeechHelperMixin):
|
||||
"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)
|
||||
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(
|
||||
"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
|
||||
@@ -442,12 +563,19 @@ class QuickMenuManager(SpeechHelperMixin):
|
||||
"speech", "voice"
|
||||
)
|
||||
|
||||
# Find current index
|
||||
try:
|
||||
current_index = (voices.index(current_voice)
|
||||
if current_voice else 0)
|
||||
except ValueError:
|
||||
# Find current index (handle Voxin voice|language format)
|
||||
current_index = 0
|
||||
if current_voice:
|
||||
try:
|
||||
# Try exact match first
|
||||
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
|
||||
if direction == "next":
|
||||
@@ -457,14 +585,38 @@ class QuickMenuManager(SpeechHelperMixin):
|
||||
|
||||
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(
|
||||
"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(
|
||||
new_voice, interrupt=True
|
||||
voice_name, interrupt=True
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user