Compare commits
5 Commits
9e8d0b3869
...
b8eb815a86
| Author | SHA1 | Date | |
|---|---|---|---|
| b8eb815a86 | |||
| beca468338 | |||
| a26fe26c8c | |||
| 508fd11610 | |||
| afe0e71a1d |
@@ -75,6 +75,22 @@ auto_read_incoming=True
|
|||||||
# Speak individual numbers instead of whole string.
|
# Speak individual numbers instead of whole string.
|
||||||
read_numbers_as_digits = False
|
read_numbers_as_digits = False
|
||||||
|
|
||||||
|
# Flood control: batch rapid updates instead of speaking each one
|
||||||
|
# Number of updates within rapid_update_window to trigger batching
|
||||||
|
rapid_update_threshold=5
|
||||||
|
|
||||||
|
# Time window (seconds) for detecting rapid updates
|
||||||
|
rapid_update_window=0.3
|
||||||
|
|
||||||
|
# How often to speak batched content (seconds)
|
||||||
|
batch_flush_interval=0.5
|
||||||
|
|
||||||
|
# Maximum lines to keep when batching (keeps newest, drops oldest)
|
||||||
|
max_batch_lines=100
|
||||||
|
|
||||||
|
# Only enable flood control if this many new lines appear in the window
|
||||||
|
flood_line_threshold=500
|
||||||
|
|
||||||
# genericSpeechCommand is the command that is executed for talking
|
# genericSpeechCommand is the command that is executed for talking
|
||||||
# the following variables are replaced with values
|
# the following variables are replaced with values
|
||||||
# fenrirText = is the text that should be spoken
|
# fenrirText = is the text that should be spoken
|
||||||
@@ -137,9 +153,10 @@ interrupt_on_key_press_filter=
|
|||||||
double_tap_timeout=0.2
|
double_tap_timeout=0.2
|
||||||
|
|
||||||
[general]
|
[general]
|
||||||
# Debug levels: 0=DEACTIVE, 1=ERROR, 2=WARNING, 3=INFO (most verbose)
|
# Debug levels: 0=NONE, 1=ERROR, 2=WARNING, 3=INFO (most verbose)
|
||||||
# For production use, WARNING (2) provides good balance of useful info without spam
|
# For production use, WARNING (2) provides good balance of useful info without spam
|
||||||
debug_level=2
|
# The default is 0, no logging.
|
||||||
|
debug_level=0
|
||||||
# debugMode sets where the debug output should send to:
|
# debugMode sets where the debug output should send to:
|
||||||
# debugMode=File writes to debug_file (Default:/tmp/fenrir-PID.log)
|
# debugMode=File writes to debug_file (Default:/tmp/fenrir-PID.log)
|
||||||
# debugMode=Print just prints on the screen
|
# debugMode=Print just prints on the screen
|
||||||
|
|||||||
+9
-2
@@ -71,6 +71,13 @@ class command:
|
|||||||
self.env["screen"]["new_cursor"]["y"],
|
self.env["screen"]["new_cursor"]["y"],
|
||||||
self.env["screen"]["new_content_text"],
|
self.env["screen"]["new_content_text"],
|
||||||
)
|
)
|
||||||
|
# Don't interrupt ongoing auto-read announcements
|
||||||
|
do_interrupt = True
|
||||||
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
"speech", "auto_read_incoming"
|
||||||
|
):
|
||||||
|
do_interrupt = False
|
||||||
|
|
||||||
if curr_char.isspace():
|
if curr_char.isspace():
|
||||||
# Only announce spaces during pure navigation (arrow keys)
|
# Only announce spaces during pure navigation (arrow keys)
|
||||||
# Check if this is really navigation by looking at input history
|
# Check if this is really navigation by looking at input history
|
||||||
@@ -87,14 +94,14 @@ class command:
|
|||||||
char_utils.present_char_for_review(
|
char_utils.present_char_for_review(
|
||||||
self.env,
|
self.env,
|
||||||
curr_char,
|
curr_char,
|
||||||
interrupt=True,
|
interrupt=do_interrupt,
|
||||||
announce_capital=True,
|
announce_capital=True,
|
||||||
flush=False,
|
flush=False,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
curr_char,
|
curr_char,
|
||||||
interrupt=True,
|
interrupt=do_interrupt,
|
||||||
ignore_punctuation=True,
|
ignore_punctuation=True,
|
||||||
announce_capital=True,
|
announce_capital=True,
|
||||||
flush=False,
|
flush=False,
|
||||||
|
|||||||
@@ -153,10 +153,17 @@ class command:
|
|||||||
if (len(curr_delta.strip()) != len(curr_delta) and curr_delta.strip() != ""):
|
if (len(curr_delta.strip()) != len(curr_delta) and curr_delta.strip() != ""):
|
||||||
curr_delta = curr_delta.strip()
|
curr_delta = curr_delta.strip()
|
||||||
|
|
||||||
|
# Don't interrupt ongoing auto-read announcements
|
||||||
|
do_interrupt = True
|
||||||
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
"speech", "auto_read_incoming"
|
||||||
|
):
|
||||||
|
do_interrupt = False
|
||||||
|
|
||||||
# Enhanced announcement with better handling of empty completions
|
# Enhanced announcement with better handling of empty completions
|
||||||
if curr_delta:
|
if curr_delta:
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
curr_delta, interrupt=True, announce_capital=True, flush=False
|
curr_delta, interrupt=do_interrupt, announce_capital=True, flush=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_callback(self, callback):
|
def set_callback(self, callback):
|
||||||
|
|||||||
@@ -66,8 +66,15 @@ class command:
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Don't interrupt ongoing auto-read announcements
|
||||||
|
do_interrupt = True
|
||||||
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
"speech", "auto_read_incoming"
|
||||||
|
):
|
||||||
|
do_interrupt = False
|
||||||
|
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
curr_word, interrupt=True, flush=False
|
curr_word, interrupt=do_interrupt, flush=False
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_callback(self, callback):
|
def set_callback(self, callback):
|
||||||
|
|||||||
+10
-4
@@ -30,8 +30,8 @@ class command:
|
|||||||
if self.env["runtime"]["ScreenManager"].is_screen_change():
|
if self.env["runtime"]["ScreenManager"].is_screen_change():
|
||||||
self.lastIdent = 0
|
self.lastIdent = 0
|
||||||
return
|
return
|
||||||
# this leads to problems in vim -> status line change -> no
|
# Don't announce cursor movements when auto-read is handling incoming text
|
||||||
# announcement, so we do check the lengh as hack
|
# This prevents interrupting ongoing auto-read announcements
|
||||||
if self.env["runtime"]["ScreenManager"].is_delta():
|
if self.env["runtime"]["ScreenManager"].is_delta():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -44,16 +44,22 @@ class command:
|
|||||||
self.env["screen"]["new_cursor"]["y"],
|
self.env["screen"]["new_cursor"]["y"],
|
||||||
self.env["screen"]["new_content_text"],
|
self.env["screen"]["new_content_text"],
|
||||||
)
|
)
|
||||||
|
# Don't interrupt ongoing auto-read announcements with cursor movement
|
||||||
|
do_interrupt = True
|
||||||
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
"speech", "auto_read_incoming"
|
||||||
|
):
|
||||||
|
do_interrupt = False
|
||||||
|
|
||||||
if curr_line.isspace():
|
if curr_line.isspace():
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
_("blank"), sound_icon="EmptyLine", interrupt=True, flush=False
|
_("blank"), sound_icon="EmptyLine", interrupt=do_interrupt, flush=False
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# ident
|
# ident
|
||||||
curr_ident = len(curr_line) - len(curr_line.lstrip())
|
curr_ident = len(curr_line) - len(curr_line.lstrip())
|
||||||
if self.lastIdent == -1:
|
if self.lastIdent == -1:
|
||||||
self.lastIdent = curr_ident
|
self.lastIdent = curr_ident
|
||||||
do_interrupt = True
|
|
||||||
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
"general", "auto_present_indent"
|
"general", "auto_present_indent"
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Fenrir TTY screen reader
|
||||||
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class command:
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def initialize(self, environment):
|
||||||
|
self.env = environment
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_description(self):
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
"speech", "auto_read_incoming"
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
if "pendingPromptText" not in self.env["commandBuffer"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
pending_text = self.env["commandBuffer"]["pendingPromptText"]
|
||||||
|
if not pending_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
pending_time = self.env["commandBuffer"].get("pendingPromptTime", 0)
|
||||||
|
delay = self.env["runtime"]["SettingsManager"].get_setting_as_float(
|
||||||
|
"speech", "batch_flush_interval"
|
||||||
|
)
|
||||||
|
if time.time() - pending_time < delay:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
|
pending_text, interrupt=False, flush=False
|
||||||
|
)
|
||||||
|
self.env["commandBuffer"]["pendingPromptText"] = ""
|
||||||
|
|
||||||
|
def set_callback(self, callback):
|
||||||
|
pass
|
||||||
@@ -158,6 +158,16 @@ class command:
|
|||||||
|
|
||||||
def _restore_speech(self):
|
def _restore_speech(self):
|
||||||
"""Helper method to restore speech when prompt is detected"""
|
"""Helper method to restore speech when prompt is detected"""
|
||||||
|
# If speech is already enabled, just clear flags to avoid unnecessary
|
||||||
|
# interrupts on prompt return
|
||||||
|
if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
|
||||||
|
"speech", "enabled"
|
||||||
|
):
|
||||||
|
self.env["commandBuffer"]["silenceUntilPrompt"] = False
|
||||||
|
if "enableSpeechOnKeypress" in self.env["commandBuffer"]:
|
||||||
|
self.env["commandBuffer"]["enableSpeechOnKeypress"] = False
|
||||||
|
return
|
||||||
|
|
||||||
# Disable silence mode
|
# Disable silence mode
|
||||||
self.env["commandBuffer"]["silenceUntilPrompt"] = False
|
self.env["commandBuffer"]["silenceUntilPrompt"] = False
|
||||||
# Also disable the keypress-based speech restoration since we're
|
# Also disable the keypress-based speech restoration since we're
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ from fenrirscreenreader.core.i18n import _
|
|||||||
|
|
||||||
class command:
|
class command:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
self._update_times = []
|
||||||
|
self._line_count_times = []
|
||||||
|
self._batched_text = []
|
||||||
|
self._last_flush_time = 0
|
||||||
|
self._in_flood_mode = False
|
||||||
|
|
||||||
def initialize(self, environment):
|
def initialize(self, environment):
|
||||||
self.env = environment
|
self.env = environment
|
||||||
@@ -21,6 +25,73 @@ class command:
|
|||||||
def get_description(self):
|
def get_description(self):
|
||||||
return _("Announces incoming text changes")
|
return _("Announces incoming text changes")
|
||||||
|
|
||||||
|
def _reset_flood_state(self):
|
||||||
|
self._update_times = []
|
||||||
|
self._line_count_times = []
|
||||||
|
self._batched_text = []
|
||||||
|
self._last_flush_time = 0
|
||||||
|
self._in_flood_mode = False
|
||||||
|
|
||||||
|
def _is_rapid_updates(self):
|
||||||
|
current_time = time.time()
|
||||||
|
window = self.env["runtime"]["SettingsManager"].get_setting_as_float(
|
||||||
|
"speech", "rapid_update_window"
|
||||||
|
)
|
||||||
|
threshold = self.env["runtime"]["SettingsManager"].get_setting_as_int(
|
||||||
|
"speech", "rapid_update_threshold"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._update_times = [
|
||||||
|
ts for ts in self._update_times if current_time - ts < window
|
||||||
|
]
|
||||||
|
self._update_times.append(current_time)
|
||||||
|
|
||||||
|
return len(self._update_times) >= threshold
|
||||||
|
|
||||||
|
def _is_high_volume(self, delta_text):
|
||||||
|
current_time = time.time()
|
||||||
|
window = self.env["runtime"]["SettingsManager"].get_setting_as_float(
|
||||||
|
"speech", "rapid_update_window"
|
||||||
|
)
|
||||||
|
threshold = self.env["runtime"]["SettingsManager"].get_setting_as_int(
|
||||||
|
"speech", "flood_line_threshold"
|
||||||
|
)
|
||||||
|
|
||||||
|
line_count = max(1, delta_text.count("\n") + 1)
|
||||||
|
self._line_count_times = [
|
||||||
|
(ts, count)
|
||||||
|
for ts, count in self._line_count_times
|
||||||
|
if current_time - ts < window
|
||||||
|
]
|
||||||
|
self._line_count_times.append((current_time, line_count))
|
||||||
|
|
||||||
|
total_lines = sum(count for _, count in self._line_count_times)
|
||||||
|
return total_lines >= threshold
|
||||||
|
|
||||||
|
def _add_to_batch(self, text):
|
||||||
|
new_lines = text.splitlines()
|
||||||
|
if text.endswith("\n"):
|
||||||
|
new_lines.append("")
|
||||||
|
self._batched_text.extend(new_lines)
|
||||||
|
|
||||||
|
max_lines = self.env["runtime"]["SettingsManager"].get_setting_as_int(
|
||||||
|
"speech", "max_batch_lines"
|
||||||
|
)
|
||||||
|
if len(self._batched_text) > max_lines:
|
||||||
|
self._batched_text = self._batched_text[-max_lines:]
|
||||||
|
|
||||||
|
def _flush_batch(self):
|
||||||
|
if not self._batched_text:
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "\n".join(self._batched_text)
|
||||||
|
self._batched_text = []
|
||||||
|
self._last_flush_time = time.time()
|
||||||
|
|
||||||
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
|
text, interrupt=False, flush=False
|
||||||
|
)
|
||||||
|
|
||||||
def _was_handled_by_tab_completion(self, delta_text):
|
def _was_handled_by_tab_completion(self, delta_text):
|
||||||
"""Check if this delta was already handled by tab completion to avoid duplicates"""
|
"""Check if this delta was already handled by tab completion to avoid duplicates"""
|
||||||
if "tabCompletion" not in self.env["commandBuffer"]:
|
if "tabCompletion" not in self.env["commandBuffer"]:
|
||||||
@@ -51,6 +122,9 @@ class command:
|
|||||||
|
|
||||||
delta_text = self.env["screen"]["new_delta"]
|
delta_text = self.env["screen"]["new_delta"]
|
||||||
|
|
||||||
|
if self.env["runtime"]["ScreenManager"].is_screen_change():
|
||||||
|
self._reset_flood_state()
|
||||||
|
|
||||||
# Skip if tab completion already handled this delta
|
# Skip if tab completion already handled this delta
|
||||||
if self._was_handled_by_tab_completion(delta_text):
|
if self._was_handled_by_tab_completion(delta_text):
|
||||||
return
|
return
|
||||||
@@ -71,6 +145,29 @@ class command:
|
|||||||
# <= 2:
|
# <= 2:
|
||||||
if "\n" not in delta_text:
|
if "\n" not in delta_text:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
rapid = self._is_rapid_updates()
|
||||||
|
high_volume = self._is_high_volume(delta_text)
|
||||||
|
|
||||||
|
if (rapid and high_volume) or self._in_flood_mode:
|
||||||
|
if not self._in_flood_mode:
|
||||||
|
self._last_flush_time = time.time()
|
||||||
|
self._in_flood_mode = True
|
||||||
|
|
||||||
|
self._add_to_batch(delta_text)
|
||||||
|
|
||||||
|
interval = self.env["runtime"][
|
||||||
|
"SettingsManager"
|
||||||
|
].get_setting_as_float("speech", "batch_flush_interval")
|
||||||
|
if time.time() - self._last_flush_time >= interval:
|
||||||
|
self._flush_batch()
|
||||||
|
|
||||||
|
if not rapid or not high_volume:
|
||||||
|
if self._batched_text:
|
||||||
|
self._flush_batch()
|
||||||
|
self._in_flood_mode = False
|
||||||
|
return
|
||||||
|
|
||||||
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
|
# print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
|
||||||
self.env["runtime"]["OutputManager"].present_text(
|
self.env["runtime"]["OutputManager"].present_text(
|
||||||
delta_text, interrupt=False, flush=False
|
delta_text, interrupt=False, flush=False
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ settings_data = {
|
|||||||
"language": "",
|
"language": "",
|
||||||
"auto_read_incoming": True,
|
"auto_read_incoming": True,
|
||||||
"read_numbers_as_digits": False,
|
"read_numbers_as_digits": False,
|
||||||
|
"rapid_update_threshold": 5,
|
||||||
|
"rapid_update_window": 0.3,
|
||||||
|
"batch_flush_interval": 0.5,
|
||||||
|
"max_batch_lines": 100,
|
||||||
|
"flood_line_threshold": 500,
|
||||||
"generic_speech_command": 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"',
|
"generic_speech_command": 'espeak -a fenrirVolume -s fenrirRate -p fenrirPitch -v fenrirVoice "fenrirText"',
|
||||||
"fenrir_min_volume": 0,
|
"fenrir_min_volume": 0,
|
||||||
"fenrir_max_volume": 200,
|
"fenrir_max_volume": 200,
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# Fenrir TTY screen reader
|
# Fenrir TTY screen reader
|
||||||
# By Chrys, Storm Dragon, and contributors.
|
# By Chrys, Storm Dragon, and contributors.
|
||||||
|
|
||||||
version = "2025.12.30"
|
version = "2026.01.08"
|
||||||
code_name = "master"
|
code_name = "master"
|
||||||
|
|||||||
Reference in New Issue
Block a user