Skip to content

Commit

Permalink
Add suspend/resume sub-commands
Browse files Browse the repository at this point in the history
Support process level suspend/resume, useful for large language models
which can time some time to load.
  • Loading branch information
ideasman42 committed Feb 2, 2023
1 parent 45cc645 commit 79d63bd
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 11 deletions.
38 changes: 31 additions & 7 deletions _misc/readme_update_helptext.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ def patch_help_test_all(help_output):
return help_output


def patch_help_test_main(help_output):
help_output = help_output.replace('{begin,end,cancel}', '')
def patch_help_test_main(help_output, sub_commands):
help_output = help_output.replace('{' + ','.join(sub_commands) + '}', '')
help_output = re.sub(r"[ \t]+(\n|\Z)", r"\1", help_output)

help_output = help_output.replace(" begin ", " :begin: ")
help_output = help_output.replace(" end ", " :end: ")
help_output = help_output.replace(" cancel ", " :cancel: ")
for sub_command in sub_commands:
help_output = help_output.replace(" {:s} ".format(sub_command), " :{:s}: ".format(sub_command))
return help_output


Expand All @@ -38,15 +37,40 @@ def patch_help_test_for_begin(help_output):
return help_output


def subcommands_from_help_output(help_output):
find = "\npositional arguments:\n"
i = help_output.find(find)
if i == -1:
# Should never happen, unless Python change their text.
raise Exception("Not found! " + repr(find))

i += len(find)
beg = help_output.find("{", i)
if beg == -1:
print("Error: could not find sub-command end '}'")
return None
beg += 1
end = help_output.find("}", beg)
if end == -1:
print("Error: could not find sub-command end '}'")
return None

return help_output[beg:end].split(",")


def main():
base_command = "python3", os.path.join(BASE_DIR, COMMAND_NAME)
p = subprocess.run(
[*base_command, "--help"],
stdout=subprocess.PIPE,
)
help_output = [(p.stdout.decode("utf-8").rstrip() + "\n\n")]
# Extract sub-commands.
sub_commands = subcommands_from_help_output(help_output[0])
if sub_commands is None:
return

for sub_command in ("begin", "end", "cancel"):
for sub_command in sub_commands:
p = subprocess.run([*base_command, sub_command, "--help"], stdout=subprocess.PIPE)
title = "Subcommand: ``" + sub_command + "``"
help_output.append(
Expand All @@ -61,7 +85,7 @@ def main():
help_output[i] = re.sub(r"[ \t]+(\n|\Z)", r"\1", help_output[i])
help_output[i] = patch_help_test_all(help_output[i])

help_output[0] = patch_help_test_main(help_output[0])
help_output[0] = patch_help_test_main(help_output[0], sub_commands)
help_output[1] = patch_help_test_for_begin(help_output[1])

help_output[0] = (
Expand Down
1 change: 1 addition & 0 deletions changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Changelog
#########

- 2023/02/02: Add ``suspend`` & ``resume`` sub-commands for process level suspend/resume.
- 2022/11/03: Add ``dotool`` support with ``--simpulate-input-tool=DOTOOL``.
- 2022/06/05: Add packaging script for PIP/setup-tools to optionally install via PIP.
- 2022/05/16: Add ``ydotool`` support with ``--simpulate-input-tool=YDOTOOL``.
Expand Down
96 changes: 96 additions & 0 deletions nerd-dictation
Original file line number Diff line number Diff line change
Expand Up @@ -955,9 +955,12 @@ def text_from_vosk_pipe(
def handle_fn_suspended() -> None:
nonlocal handled_any
nonlocal text_prev
nonlocal json_text_partial_prev

handled_any = False
text_prev = ""
json_text_partial_prev = ""

if not (progressive and progressive_continuous):
text_list.clear()

Expand Down Expand Up @@ -1211,6 +1214,10 @@ def main_begin(
is_run_on = age_in_seconds is not None and (age_in_seconds < punctuate_from_previous_timeout)
del age_in_seconds

# Write the PID, needed for suspend/resume sub-commands to know the PID of the current process.
with open(path_to_cookie, "w", encoding="utf-8") as fh:
fh.write(str(os.getpid()))

# Force zero time-stamp so a fast begin/end (tap) action
# doesn't leave dictation running.
touch(path_to_cookie, mtime=0)
Expand Down Expand Up @@ -1350,6 +1357,9 @@ def main_end(
if not path_to_cookie:
path_to_cookie = os.path.join(tempfile.gettempdir(), TEMP_COOKIE_NAME)

# Resume (does nothing if not suspended), so suspending doesn't prevent the cancel operation.
main_suspend(path_to_cookie=path_to_cookie, suspend=False, verbose=False)

touch(path_to_cookie)


Expand All @@ -1360,9 +1370,43 @@ def main_cancel(
if not path_to_cookie:
path_to_cookie = os.path.join(tempfile.gettempdir(), TEMP_COOKIE_NAME)

# Resume (does nothing if not suspended), so suspending doesn't prevent the cancel operation.
main_suspend(path_to_cookie=path_to_cookie, suspend=False, verbose=False)

file_remove_if_exists(path_to_cookie)


def main_suspend(
*,
path_to_cookie: str = "",
suspend: bool,
verbose: bool,
) -> None:
import signal

if not path_to_cookie:
path_to_cookie = os.path.join(tempfile.gettempdir(), TEMP_COOKIE_NAME)

if not os.path.exists(path_to_cookie):
if verbose:
sys.stderr.write("No running nerd-dictation cookie found at: {:s}, abort!\n".format(path_to_cookie))
return

with open(path_to_cookie, "r", encoding="utf-8") as fh:
data = fh.read()
try:
pid = int(data)
except Exception as ex:
if verbose:
sys.stderr.write("Failed to read PID with error {!r}, abort!\n".format(ex))
return

if suspend:
os.kill(pid, signal.SIGUSR1)
else: # Resume.
os.kill(pid, signal.SIGCONT)


def argparse_generic_command_cookie(subparse: argparse.ArgumentParser) -> None:
subparse.add_argument(
"--cookie",
Expand Down Expand Up @@ -1696,15 +1740,67 @@ def argparse_create_cancel(subparsers: "argparse._SubParsersAction[argparse.Argu
)


def argparse_create_suspend(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
subparse = subparsers.add_parser(
"suspend",
help="Suspend the dictation process.",
description=(
"Suspend recording audio & the dictation process.\n"
"\n"
"This is useful on slower systems or when large language models take longer to load.\n"
"Recording audio is stopped and the process is paused to remove any CPU overhead."
),
formatter_class=argparse.RawTextHelpFormatter,
)

argparse_generic_command_cookie(subparse)

subparse.set_defaults(
func=lambda args: main_suspend(
path_to_cookie=args.path_to_cookie,
suspend=True,
verbose=True,
),
)


def argparse_create_resume(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
subparse = subparsers.add_parser(
"resume",
help="Resume the dictation process.",
description=(
"Resume recording audio & the dictation process.\n"
"\n"
"This is to be used to resume after the 'suspend' command.\n"
"When nerd-dictation is not suspended, this does nothing.\n"
),
formatter_class=argparse.RawTextHelpFormatter,
)

argparse_generic_command_cookie(subparse)

subparse.set_defaults(
func=lambda args: main_suspend(
path_to_cookie=args.path_to_cookie,
suspend=False,
verbose=True,
),
)


def argparse_create() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter)

subparsers = parser.add_subparsers()

argparse_create_begin(subparsers)

argparse_create_end(subparsers)
argparse_create_cancel(subparsers)

argparse_create_suspend(subparsers)
argparse_create_resume(subparsers)

return parser


Expand Down
42 changes: 38 additions & 4 deletions readme.rst
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,14 @@ While it could use any system currently it uses the VOSK-API.

positional arguments:

:begin: Begin dictation.
:end: End dictation.
:cancel: Cancel dictation.
:begin: Begin dictation.
:end: End dictation.
:cancel: Cancel dictation.
:suspend: Suspend the dictation process.
:resume: Resume the dictation process.

options:
-h, --help show this help message and exit
-h, --help show this help message and exit

Subcommand: ``begin``
---------------------
Expand Down Expand Up @@ -297,6 +299,38 @@ usage::

This cancels dictation.

options:
-h, --help show this help message and exit
--cookie FILE_PATH Location for writing a temporary cookie (this file is monitored to begin/end dictation).

Subcommand: ``suspend``
-----------------------

usage::

nerd-dictation suspend [-h] [--cookie FILE_PATH]

Suspend recording audio & the dictation process.

This is useful on slower systems or when large language models take longer to load.
Recording audio is stopped and the process is paused to remove any CPU overhead.

options:
-h, --help show this help message and exit
--cookie FILE_PATH Location for writing a temporary cookie (this file is monitored to begin/end dictation).

Subcommand: ``resume``
----------------------

usage::

nerd-dictation resume [-h] [--cookie FILE_PATH]

Resume recording audio & the dictation process.

This is to be used to resume after the 'suspend' command.
When nerd-dictation is not suspended, this does nothing.

options:
-h, --help show this help message and exit
--cookie FILE_PATH Location for writing a temporary cookie (this file is monitored to begin/end dictation).
Expand Down

0 comments on commit 79d63bd

Please sign in to comment.