Fixed 2 speech-dispatcher bugs. First, multiword voice names were not available. Next, when the module changed the old voice name was carried in causing some modules to fail.
This commit is contained in:
@@ -4,6 +4,7 @@ import subprocess
|
||||
import time
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
from fenrirscreenreader.utils.speechd_utils import get_synthesis_voice_name
|
||||
|
||||
|
||||
class command:
|
||||
@@ -273,29 +274,13 @@ class command:
|
||||
for line in lines[1:]:
|
||||
if not line.strip():
|
||||
continue
|
||||
if module.lower() == "espeak-ng":
|
||||
voice = self.process_espeak_voice(line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
else:
|
||||
voices.append(line.strip())
|
||||
voice = get_synthesis_voice_name(module, line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
return voices
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
def process_espeak_voice(self, voiceLine):
|
||||
"""Process espeak voice format"""
|
||||
parts = [p for p in voiceLine.split() if p]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
lang_code = parts[-2].lower()
|
||||
variant = parts[-1].lower()
|
||||
return (
|
||||
f"{lang_code}+{variant}"
|
||||
if variant and variant != "none"
|
||||
else lang_code
|
||||
)
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
|
||||
@@ -5,6 +5,7 @@ import threading
|
||||
import time
|
||||
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
from fenrirscreenreader.utils.speechd_utils import get_synthesis_voice_name
|
||||
|
||||
|
||||
class command:
|
||||
@@ -179,12 +180,9 @@ class command:
|
||||
for line in lines[1:]:
|
||||
if not line.strip():
|
||||
continue
|
||||
if module.lower() == "espeak-ng":
|
||||
voice = self.process_espeak_voice(line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
else:
|
||||
voices.append(line.strip())
|
||||
voice = get_synthesis_voice_name(module, line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
|
||||
# Limit voice count to prevent memory issues
|
||||
if len(voices) > 1000:
|
||||
@@ -205,21 +203,5 @@ class command:
|
||||
)
|
||||
return []
|
||||
|
||||
def process_espeak_voice(self, voiceLine):
|
||||
"""Process espeak voice format"""
|
||||
try:
|
||||
parts = [p for p in voiceLine.split() if p]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
lang_code = parts[-2].lower()
|
||||
variant = parts[-1].lower()
|
||||
return (
|
||||
f"{lang_code}+{variant}"
|
||||
if variant and variant != "none"
|
||||
else lang_code
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def set_callback(self, callback):
|
||||
pass
|
||||
|
||||
@@ -79,10 +79,11 @@ class command:
|
||||
# updates
|
||||
delta_text = self.env["screen"]["new_delta"]
|
||||
delta_length = len(delta_text)
|
||||
explicit_progress_lines = self.get_explicit_progress_lines(delta_text)
|
||||
if (
|
||||
delta_length > 200
|
||||
): # Allow longer progress lines such as terminal status output
|
||||
if not self.is_explicit_progress_delta(delta_text):
|
||||
if not explicit_progress_lines:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"Progress filter: delta too long ({delta_length})",
|
||||
debug.DebugLevel.INFO,
|
||||
@@ -91,7 +92,11 @@ class command:
|
||||
|
||||
# If delta contains newlines and is substantial, let incoming handler
|
||||
# deal with it to avoid interfering with multi-line text output
|
||||
if '\n' in delta_text and delta_length > 50:
|
||||
if (
|
||||
"\n" in delta_text
|
||||
and delta_length > 50
|
||||
and not explicit_progress_lines
|
||||
):
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
f"Progress filter: multiline delta ({delta_length} chars)",
|
||||
debug.DebugLevel.INFO,
|
||||
@@ -119,13 +124,35 @@ class command:
|
||||
if not has_percentage:
|
||||
return self.has_progress_status_fields(text)
|
||||
|
||||
return bool(
|
||||
re.search(
|
||||
r"[|\[\]#=*>█▉▊▋▌▍▎▏▒▓░]"
|
||||
r"|\b\d+(?:\.\d+)?\s*[kKmMgGtT](?:i?B)?/s\b",
|
||||
text,
|
||||
)
|
||||
bracketed_bar = re.search(
|
||||
r"\[(?=[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]{2,}\])"
|
||||
r"(?=[^\]]*[#=*>█▉▊▋▌▍▎▏▒▓░])"
|
||||
r"[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]+\]",
|
||||
text,
|
||||
)
|
||||
pipe_bar = re.search(
|
||||
r"\|(?=[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]{2,}\|)"
|
||||
r"(?=[^|]*[#=*>█▉▊▋▌▍▎▏▒▓░])"
|
||||
r"[#=*>█▉▊▋▌▍▎▏▒▓░.\s-]+\|",
|
||||
text,
|
||||
)
|
||||
transfer_rate = re.search(
|
||||
r"\b\d+(?:\.\d+)?\s*[kKmMgGtT](?:i?B)?/s\b",
|
||||
text,
|
||||
)
|
||||
return bool(
|
||||
bracketed_bar
|
||||
or pipe_bar
|
||||
or transfer_rate
|
||||
)
|
||||
|
||||
def get_explicit_progress_lines(self, text):
|
||||
"""Return structurally explicit progress rows from a screen delta."""
|
||||
return [
|
||||
line
|
||||
for line in text.splitlines()
|
||||
if self.is_explicit_progress_delta(line)
|
||||
]
|
||||
|
||||
def reset_progress_state(self):
|
||||
"""Reset progress state when a prompt is detected, allowing new progress operations to start fresh"""
|
||||
@@ -140,6 +167,15 @@ class command:
|
||||
import re
|
||||
import time
|
||||
|
||||
if "\n" in text:
|
||||
explicit_progress_lines = self.get_explicit_progress_lines(text)
|
||||
if not explicit_progress_lines:
|
||||
return
|
||||
# Parallel transfer tools commonly repaint several package rows,
|
||||
# followed by an aggregate row. One tone per update is enough, and
|
||||
# the final explicit row is normally that aggregate.
|
||||
text = explicit_progress_lines[-1]
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
# Debug: Print what we're checking
|
||||
|
||||
@@ -6,6 +6,7 @@ import subprocess
|
||||
import time
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.utils.speechd_utils import get_synthesis_voice_name
|
||||
|
||||
|
||||
class DynamicVoiceCommand:
|
||||
@@ -354,30 +355,10 @@ def get_module_voices(module):
|
||||
for line in lines[1:]:
|
||||
if not line.strip():
|
||||
continue
|
||||
if module.lower() == "espeak-ng":
|
||||
voice = process_espeak_voice(line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
else:
|
||||
voices.append(line.strip())
|
||||
voice = get_synthesis_voice_name(module, line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
return voices
|
||||
except Exception:
|
||||
pass
|
||||
return []
|
||||
|
||||
|
||||
def process_espeak_voice(voiceLine):
|
||||
"""Process espeak voice format"""
|
||||
try:
|
||||
parts = [p for p in voiceLine.split() if p]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
lang_code = parts[-2].lower()
|
||||
variant = parts[-1].lower()
|
||||
return (
|
||||
f"{lang_code}+{variant}"
|
||||
if variant and variant != "none"
|
||||
else lang_code
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -4,11 +4,16 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from fenrirscreenreader.core import debug
|
||||
from fenrirscreenreader.core.i18n import _
|
||||
from fenrirscreenreader.core.settingsData import settings_data
|
||||
import subprocess
|
||||
import time
|
||||
from fenrirscreenreader.utils.speechd_utils import (
|
||||
get_synthesis_voice_name,
|
||||
parse_synthesis_voice_line,
|
||||
)
|
||||
|
||||
|
||||
class SpeechHelperMixin:
|
||||
@@ -89,20 +94,15 @@ class SpeechHelperMixin:
|
||||
for line in lines[1:]:
|
||||
if not line.strip():
|
||||
continue
|
||||
if module.lower() == "espeak-ng":
|
||||
voice = self._process_espeak_voice(line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
elif module.lower() == "voxin":
|
||||
if 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 other modules, extract first field (voice name)
|
||||
parts = line.strip().split()
|
||||
if parts:
|
||||
voices.append(parts[0])
|
||||
voice = get_synthesis_voice_name(module, line)
|
||||
if voice:
|
||||
voices.append(voice)
|
||||
|
||||
self._voices_cache[module] = voices
|
||||
return voices
|
||||
@@ -115,23 +115,6 @@ class SpeechHelperMixin:
|
||||
|
||||
return []
|
||||
|
||||
def _process_espeak_voice(self, voice_line):
|
||||
"""Process espeak-ng voice format into usable voice name.
|
||||
|
||||
Args:
|
||||
voice_line (str): Raw line from spd-say -L output
|
||||
|
||||
Returns:
|
||||
str: Processed voice name (e.g., 'en-us' or 'en-us+f3')
|
||||
"""
|
||||
parts = [p for p in voice_line.split() if p]
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
lang_code = parts[-2].lower()
|
||||
variant = parts[-1].lower()
|
||||
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.
|
||||
|
||||
@@ -142,11 +125,12 @@ class SpeechHelperMixin:
|
||||
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:
|
||||
voice_data = parse_synthesis_voice_line(voice_line)
|
||||
if voice_data is None:
|
||||
return None
|
||||
voice_name, language, _variant = voice_data
|
||||
if not language:
|
||||
return None
|
||||
voice_name = parts[0]
|
||||
language = parts[1]
|
||||
# Encode language with voice for later extraction
|
||||
return f"{voice_name}|{language}"
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2026.07.19"
|
||||
version = "2026.07.25"
|
||||
code_name = "testing"
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
|
||||
def parse_synthesis_voice_line(voice_line):
|
||||
"""Parse one column-aligned spd-say synthesis voice row."""
|
||||
fields = voice_line.rsplit(maxsplit=2)
|
||||
if len(fields) != 3:
|
||||
return None
|
||||
|
||||
name, language, variant = (field.strip() for field in fields)
|
||||
if not name:
|
||||
return None
|
||||
return name, language, variant
|
||||
|
||||
|
||||
def get_synthesis_voice_name(module, voice_line):
|
||||
"""Return the value Speech Dispatcher expects for voice selection."""
|
||||
voice_data = parse_synthesis_voice_line(voice_line)
|
||||
if voice_data is None:
|
||||
return None
|
||||
|
||||
name, language, variant = voice_data
|
||||
if module.lower() != "espeak-ng":
|
||||
return name
|
||||
|
||||
language = language.lower()
|
||||
variant = variant.lower()
|
||||
if not language:
|
||||
return None
|
||||
if variant and variant != "none":
|
||||
return f"{language}+{variant}"
|
||||
return language
|
||||
Reference in New Issue
Block a user