Speculative fix for a sometimes speech crash bug.
This commit is contained in:
@@ -22,6 +22,8 @@ class OutputManager:
|
||||
self.interrupt_thread = None
|
||||
self.interrupt_done = None
|
||||
self.interrupt_wait_timeout = 0.1
|
||||
self.speech_driver_lock = threading.Lock()
|
||||
self.speech_driver_lock_timeout = 0.05
|
||||
|
||||
def initialize(self, environment):
|
||||
self.env = environment
|
||||
@@ -159,132 +161,144 @@ class OutputManager:
|
||||
return
|
||||
if interrupt or flush:
|
||||
self.interrupt_output()
|
||||
if not self.speech_driver_lock.acquire(
|
||||
timeout=self.speech_driver_lock_timeout
|
||||
):
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"OutputManager.speak_text: Speech driver busy, dropping speech",
|
||||
debug.DebugLevel.WARNING,
|
||||
)
|
||||
return
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_language(
|
||||
self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "language"
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_language(
|
||||
self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "language"
|
||||
)
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech language in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_voice(
|
||||
self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "voice"
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech language in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"Error while setting speech voice in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
if announce_capital:
|
||||
self.env["runtime"]["SpeechDriver"].set_pitch(
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_voice(
|
||||
self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "voice"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"Error while setting speech voice in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
if announce_capital:
|
||||
self.env["runtime"]["SpeechDriver"].set_pitch(
|
||||
self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_float("speech", "capital_pitch")
|
||||
)
|
||||
else:
|
||||
self.env["runtime"]["SpeechDriver"].set_pitch(
|
||||
self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_float("speech", "pitch")
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech pitch in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_rate(
|
||||
self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_float("speech", "capital_pitch")
|
||||
].get_setting_as_float("speech", "rate")
|
||||
)
|
||||
else:
|
||||
self.env["runtime"]["SpeechDriver"].set_pitch(
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech rate in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_module(
|
||||
self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "module"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech module in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_volume(
|
||||
self.env["runtime"][
|
||||
"SettingsManager"
|
||||
].get_setting_as_float("speech", "pitch")
|
||||
].get_setting_as_float("speech", "volume")
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech pitch in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_rate(
|
||||
self.env["runtime"]["SettingsManager"].get_setting_as_float(
|
||||
"speech", "rate"
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech volume in OutputManager.speak_text ",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech rate in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_module(
|
||||
self.env["runtime"]["SettingsManager"].get_setting(
|
||||
"speech", "module"
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech module in OutputManager.speak_text",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].set_volume(
|
||||
self.env["runtime"]["SettingsManager"].get_setting_as_float(
|
||||
"speech", "volume"
|
||||
try:
|
||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||
"general", "new_line_pause"
|
||||
):
|
||||
clean_text = text.replace("\n", " , ")
|
||||
else:
|
||||
clean_text = text.replace("\n", " ")
|
||||
|
||||
clean_text = self.env["runtime"][
|
||||
"TextManager"
|
||||
].replace_head_lines(clean_text)
|
||||
clean_text = self.process_mid_word_punctuation(clean_text)
|
||||
clean_text = self.env["runtime"][
|
||||
"PunctuationManager"
|
||||
].proceed_punctuation(clean_text, ignore_punctuation)
|
||||
clean_text = re.sub(" +$", " ", clean_text)
|
||||
self.env["runtime"]["SpeechDriver"].speak(
|
||||
clean_text, True, ignore_punctuation
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"setting speech volume in OutputManager.speak_text ",
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
|
||||
try:
|
||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||
"general", "new_line_pause"
|
||||
):
|
||||
clean_text = text.replace("\n", " , ")
|
||||
else:
|
||||
clean_text = text.replace("\n", " ")
|
||||
|
||||
clean_text = self.env["runtime"]["TextManager"].replace_head_lines(
|
||||
clean_text
|
||||
)
|
||||
clean_text = self.process_mid_word_punctuation(clean_text)
|
||||
clean_text = self.env["runtime"][
|
||||
"PunctuationManager"
|
||||
].proceed_punctuation(clean_text, ignore_punctuation)
|
||||
clean_text = re.sub(" +$", " ", clean_text)
|
||||
self.env["runtime"]["SpeechDriver"].speak(
|
||||
clean_text, True, ignore_punctuation
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"Speak: " + clean_text, debug.DebugLevel.INFO
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
'"speak" in OutputManager.speak_text ', debug.DebugLevel.ERROR
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"Speak: " + clean_text, debug.DebugLevel.INFO
|
||||
)
|
||||
except Exception as e:
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
'"speak" in OutputManager.speak_text ',
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
str(e), debug.DebugLevel.ERROR
|
||||
)
|
||||
finally:
|
||||
self.speech_driver_lock.release()
|
||||
|
||||
def interrupt_output(self, wait=True):
|
||||
interrupt_done, started = self.start_interrupt_output()
|
||||
@@ -320,6 +334,15 @@ class OutputManager:
|
||||
self.interrupt_running = False
|
||||
|
||||
def cancel_speech(self):
|
||||
if not self.speech_driver_lock.acquire(
|
||||
timeout=self.speech_driver_lock_timeout
|
||||
):
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
"OutputManager interrupt_output: Speech driver busy, "
|
||||
"skipping interrupt",
|
||||
debug.DebugLevel.WARNING,
|
||||
)
|
||||
return
|
||||
try:
|
||||
self.env["runtime"]["SpeechDriver"].cancel()
|
||||
self.env["runtime"]["DebugManager"].write_debug_out(
|
||||
@@ -331,6 +354,8 @@ class OutputManager:
|
||||
+ str(e),
|
||||
debug.DebugLevel.ERROR,
|
||||
)
|
||||
finally:
|
||||
self.speech_driver_lock.release()
|
||||
|
||||
def play_sound_icon(self, sound_icon="", interrupt=True):
|
||||
if sound_icon == "":
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
# Fenrir TTY screen reader
|
||||
# By Chrys, Storm Dragon, and contributors.
|
||||
|
||||
version = "2026.05.18"
|
||||
version = "2026.05.19"
|
||||
code_name = "testing"
|
||||
|
||||
@@ -26,6 +26,14 @@ def build_output_manager():
|
||||
"SoundDriver": sound_driver,
|
||||
"SpeechDriver": speech_driver,
|
||||
"DebugManager": Mock(write_debug_out=Mock()),
|
||||
"TextManager": Mock(
|
||||
replace_head_lines=Mock(side_effect=lambda text: text)
|
||||
),
|
||||
"PunctuationManager": Mock(
|
||||
proceed_punctuation=Mock(
|
||||
side_effect=lambda text, _ignore_punctuation: text
|
||||
)
|
||||
),
|
||||
},
|
||||
}
|
||||
return output_manager, sound_driver, speech_driver
|
||||
@@ -122,6 +130,40 @@ def test_interrupt_output_waits_only_briefly_for_slow_cancel():
|
||||
output_manager.interrupt_thread.join(timeout=1.0)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_cancel_speech_skips_when_speech_driver_is_busy():
|
||||
output_manager, _sound_driver, speech_driver = build_output_manager()
|
||||
output_manager.speech_driver_lock_timeout = 0.01
|
||||
output_manager.speech_driver_lock.acquire()
|
||||
|
||||
try:
|
||||
start_time = time.monotonic()
|
||||
output_manager.cancel_speech()
|
||||
elapsed = time.monotonic() - start_time
|
||||
finally:
|
||||
output_manager.speech_driver_lock.release()
|
||||
|
||||
assert elapsed < 0.2
|
||||
speech_driver.cancel.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_speak_text_drops_speech_when_cancel_holds_driver_lock():
|
||||
output_manager, _sound_driver, speech_driver = build_output_manager()
|
||||
output_manager.speech_driver_lock_timeout = 0.01
|
||||
output_manager.speech_driver_lock.acquire()
|
||||
|
||||
try:
|
||||
start_time = time.monotonic()
|
||||
output_manager.speak_text("hello", interrupt=False, flush=False)
|
||||
elapsed = time.monotonic() - start_time
|
||||
finally:
|
||||
output_manager.speech_driver_lock.release()
|
||||
|
||||
assert elapsed < 0.2
|
||||
speech_driver.speak.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_key_interrupt_command_uses_nonblocking_interrupt():
|
||||
module = load_key_interrupt_module()
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import json
|
||||
import time
|
||||
|
||||
from fenrirscreenreader.core import remoteInstanceRegistry
|
||||
|
||||
|
||||
def test_write_instance_prunes_stale_registry_files(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
remoteInstanceRegistry, "get_registry_dir", lambda: str(tmp_path)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
remoteInstanceRegistry,
|
||||
"process_exists",
|
||||
lambda pid: pid == 456,
|
||||
)
|
||||
|
||||
stale_instance = tmp_path / "123.json"
|
||||
stale_instance.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"pid": 123,
|
||||
"updated_at": time.time(),
|
||||
}
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
invalid_instance = tmp_path / "invalid.json"
|
||||
invalid_instance.write_text("not json\n", encoding="utf-8")
|
||||
|
||||
remoteInstanceRegistry.write_instance({"pid": 456})
|
||||
|
||||
assert not stale_instance.exists()
|
||||
assert not invalid_instance.exists()
|
||||
assert (tmp_path / "456.json").exists()
|
||||
Reference in New Issue
Block a user