Progress bar beeps now work on ffmpeg output.
This commit is contained in:
@@ -117,7 +117,7 @@ class command:
|
|||||||
|
|
||||||
has_percentage = re.search(r"(^|\s)\d+(?:\.\d+)?\s*%", text)
|
has_percentage = re.search(r"(^|\s)\d+(?:\.\d+)?\s*%", text)
|
||||||
if not has_percentage:
|
if not has_percentage:
|
||||||
return False
|
return self.has_progress_status_fields(text)
|
||||||
|
|
||||||
return bool(
|
return bool(
|
||||||
re.search(
|
re.search(
|
||||||
@@ -240,8 +240,20 @@ class command:
|
|||||||
pacman_match = re.search(
|
pacman_match = re.search(
|
||||||
r"\d+(?:\.\d+)?\s+[kKmMgGtT]iB\s+\d+(?:\.\d+)?\s+[kKmMgGtT]iB/s\s+\d+:\d+", text
|
r"\d+(?:\.\d+)?\s+[kKmMgGtT]iB\s+\d+(?:\.\d+)?\s+[kKmMgGtT]iB/s\s+\d+:\d+", text
|
||||||
)
|
)
|
||||||
|
# Pattern 1g: Key/value status lines from ffmpeg-like terminal tools.
|
||||||
|
# These do not expose a percentage in the status line, so treat them as
|
||||||
|
# activity rather than guessing progress from elapsed time.
|
||||||
|
status_fields_match = self.has_progress_status_fields(text)
|
||||||
|
|
||||||
if time_match or token_match or dd_match or curl_match or transfer_match or pacman_match:
|
if (
|
||||||
|
time_match
|
||||||
|
or token_match
|
||||||
|
or dd_match
|
||||||
|
or curl_match
|
||||||
|
or transfer_match
|
||||||
|
or pacman_match
|
||||||
|
or status_fields_match
|
||||||
|
):
|
||||||
# For non-percentage progress, use a single activity beep every 2
|
# For non-percentage progress, use a single activity beep every 2
|
||||||
# seconds
|
# seconds
|
||||||
if (
|
if (
|
||||||
@@ -577,5 +589,48 @@ class command:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def has_progress_status_fields(self, text):
|
||||||
|
"""Recognize dense key=value status lines from terminal tools."""
|
||||||
|
import re
|
||||||
|
|
||||||
|
if "\n" in text or self.contains_url(text):
|
||||||
|
return False
|
||||||
|
|
||||||
|
fields = re.findall(r"\b([a-zA-Z][a-zA-Z0-9_-]*)=\s*\S+", text)
|
||||||
|
if len(fields) < 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
field_names = {field.lower() for field in fields}
|
||||||
|
progress_fields = {
|
||||||
|
"frame",
|
||||||
|
"fps",
|
||||||
|
"size",
|
||||||
|
"time",
|
||||||
|
"bitrate",
|
||||||
|
"speed",
|
||||||
|
"elapsed",
|
||||||
|
"dup",
|
||||||
|
"drop",
|
||||||
|
}
|
||||||
|
if len(field_names & progress_fields) < 3:
|
||||||
|
return False
|
||||||
|
|
||||||
|
has_time_field = bool(
|
||||||
|
re.search(
|
||||||
|
r"\b(?:time|elapsed|eta)=\s*\d+(?::\d{2}){1,2}(?:\.\d+)?\b",
|
||||||
|
text,
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
has_rate_field = bool(
|
||||||
|
re.search(
|
||||||
|
r"\b(?:speed|fps|bitrate)=\s*[\d.]+[a-zA-Z/]*\b",
|
||||||
|
text,
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return has_time_field and has_rate_field
|
||||||
|
|
||||||
def set_callback(self, callback):
|
def set_callback(self, callback):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -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 = "2026.06.27"
|
version = "2026.07.12"
|
||||||
code_name = "testing"
|
code_name = "testing"
|
||||||
|
|||||||
@@ -140,3 +140,101 @@ def test_progress_detector_beeps_for_interruptible_status_without_ellipsis():
|
|||||||
command.run()
|
command.run()
|
||||||
|
|
||||||
command.play_activity_beep.assert_called_once_with()
|
command.play_activity_beep.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_progress_detector_allows_long_key_value_status_delta():
|
||||||
|
progress_module = _load_progress_module()
|
||||||
|
command = progress_module.command()
|
||||||
|
sample = (
|
||||||
|
"frame=33480 fps=429 q=28.0 size= 195328KiB "
|
||||||
|
"time=00:23:16.26 bitrate=1146.0kbits/s dup=0 drop=1 "
|
||||||
|
"speed=17.9x elapsed=0:01:18.01 "
|
||||||
|
"filter=scale,format output=archive-video-final-render-pass.mkv "
|
||||||
|
"metadata=preserve-chapters-and-stream-layout"
|
||||||
|
)
|
||||||
|
command.env = {
|
||||||
|
"commandBuffer": {"progress_monitoring": True},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
|
||||||
|
"CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)),
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"new_delta": sample,
|
||||||
|
"new_content_text": sample,
|
||||||
|
"old_cursor": {"x": 0, "y": 0},
|
||||||
|
"new_cursor": {"x": 0, "y": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(sample) > 200
|
||||||
|
assert command.is_real_progress_update()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_progress_detector_beeps_for_ffmpeg_status_line():
|
||||||
|
progress_module = _load_progress_module()
|
||||||
|
command = progress_module.command()
|
||||||
|
sample = (
|
||||||
|
"frame=33480 fps=429 q=28.0 size= 195328KiB "
|
||||||
|
"time=00:23:16.26 bitrate=1146.0kbits/s dup=0 drop=1 "
|
||||||
|
"speed=17.9x elapsed=0:01:18.01"
|
||||||
|
)
|
||||||
|
command.env = {
|
||||||
|
"commandBuffer": {
|
||||||
|
"progress_monitoring": True,
|
||||||
|
"lastProgressValue": -1,
|
||||||
|
"lastProgressTime": 0,
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
|
||||||
|
"CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)),
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"new_delta": sample,
|
||||||
|
"new_delta_is_typing": False,
|
||||||
|
"new_content_text": sample,
|
||||||
|
"old_cursor": {"x": 0, "y": 0},
|
||||||
|
"new_cursor": {"x": 0, "y": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
command.play_activity_beep = Mock()
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
command.play_activity_beep.assert_called_once_with()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.unit
|
||||||
|
def test_progress_detector_ignores_generic_key_value_text():
|
||||||
|
progress_module = _load_progress_module()
|
||||||
|
command = progress_module.command()
|
||||||
|
sample = "host=server mode=debug user=Username retry=3 status=ready"
|
||||||
|
command.env = {
|
||||||
|
"commandBuffer": {
|
||||||
|
"progress_monitoring": True,
|
||||||
|
"lastProgressValue": -1,
|
||||||
|
"lastProgressTime": 0,
|
||||||
|
},
|
||||||
|
"runtime": {
|
||||||
|
"DebugManager": Mock(write_debug_out=Mock()),
|
||||||
|
"ScreenManager": Mock(is_screen_change=Mock(return_value=False)),
|
||||||
|
"CursorManager": Mock(is_cursor_vertical_move=Mock(return_value=False)),
|
||||||
|
},
|
||||||
|
"screen": {
|
||||||
|
"new_delta": sample,
|
||||||
|
"new_delta_is_typing": False,
|
||||||
|
"new_content_text": sample,
|
||||||
|
"old_cursor": {"x": 0, "y": 0},
|
||||||
|
"new_cursor": {"x": 0, "y": 0},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
command.play_activity_beep = Mock()
|
||||||
|
command.play_progress_tone = Mock()
|
||||||
|
|
||||||
|
command.run()
|
||||||
|
|
||||||
|
command.play_activity_beep.assert_not_called()
|
||||||
|
command.play_progress_tone.assert_not_called()
|
||||||
|
|||||||
Reference in New Issue
Block a user