Compare commits
2 Commits
d46d8de3ee
...
c66a9ba9c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c66a9ba9c2 | ||
|
|
2092a3e257 |
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user