Skip to content

Commit ff69c99

Browse files
committed
Add suspend/resume sub-commands
Support process level suspend/resume, useful for large language models which can time some time to load.
1 parent 45cc645 commit ff69c99

File tree

4 files changed

+166
-11
lines changed

4 files changed

+166
-11
lines changed

_misc/readme_update_helptext.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,12 @@ def patch_help_test_all(help_output):
1919
return help_output
2020

2121

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

26-
help_output = help_output.replace(" begin ", " :begin: ")
27-
help_output = help_output.replace(" end ", " :end: ")
28-
help_output = help_output.replace(" cancel ", " :cancel: ")
26+
for sub_command in sub_commands:
27+
help_output = help_output.replace(" {:s} ".format(sub_command), " :{:s}: ".format(sub_command))
2928
return help_output
3029

3130

@@ -38,15 +37,40 @@ def patch_help_test_for_begin(help_output):
3837
return help_output
3938

4039

40+
def subcommands_from_help_output(help_output):
41+
find = "\npositional arguments:\n"
42+
i = help_output.find(find)
43+
if i == -1:
44+
# Should never happen, unless Python change their text.
45+
raise Exception("Not found! " + repr(find))
46+
47+
i += len(find)
48+
beg = help_output.find("{", i)
49+
if beg == -1:
50+
print("Error: could not find sub-command end '}'")
51+
return None
52+
beg += 1
53+
end = help_output.find("}", beg)
54+
if end == -1:
55+
print("Error: could not find sub-command end '}'")
56+
return None
57+
58+
return help_output[beg:end].split(",")
59+
60+
4161
def main():
4262
base_command = "python3", os.path.join(BASE_DIR, COMMAND_NAME)
4363
p = subprocess.run(
4464
[*base_command, "--help"],
4565
stdout=subprocess.PIPE,
4666
)
4767
help_output = [(p.stdout.decode("utf-8").rstrip() + "\n\n")]
68+
# Extract sub-commands.
69+
sub_commands = subcommands_from_help_output(help_output[0])
70+
if sub_commands is None:
71+
return
4872

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

64-
help_output[0] = patch_help_test_main(help_output[0])
88+
help_output[0] = patch_help_test_main(help_output[0], sub_commands)
6589
help_output[1] = patch_help_test_for_begin(help_output[1])
6690

6791
help_output[0] = (

changelog.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Changelog
44
#########
55

6+
- 2023/02/02: Add ``suspend`` & ``resume`` sub-commands for process level suspend/resume.
67
- 2022/11/03: Add ``dotool`` support with ``--simpulate-input-tool=DOTOOL``.
78
- 2022/06/05: Add packaging script for PIP/setup-tools to optionally install via PIP.
89
- 2022/05/16: Add ``ydotool`` support with ``--simpulate-input-tool=YDOTOOL``.

nerd-dictation

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,9 +955,12 @@ def text_from_vosk_pipe(
955955
def handle_fn_suspended() -> None:
956956
nonlocal handled_any
957957
nonlocal text_prev
958+
nonlocal json_text_partial_prev
958959

959960
handled_any = False
960961
text_prev = ""
962+
json_text_partial_prev = ""
963+
961964
if not (progressive and progressive_continuous):
962965
text_list.clear()
963966

@@ -1211,6 +1214,10 @@ def main_begin(
12111214
is_run_on = age_in_seconds is not None and (age_in_seconds < punctuate_from_previous_timeout)
12121215
del age_in_seconds
12131216

1217+
# Write the PID, needed for suspend/resume sub-commands to know the PID of the current process.
1218+
with open(path_to_cookie, "w", encoding="utf-8") as fh:
1219+
fh.write(str(os.getpid()))
1220+
12141221
# Force zero time-stamp so a fast begin/end (tap) action
12151222
# doesn't leave dictation running.
12161223
touch(path_to_cookie, mtime=0)
@@ -1350,6 +1357,9 @@ def main_end(
13501357
if not path_to_cookie:
13511358
path_to_cookie = os.path.join(tempfile.gettempdir(), TEMP_COOKIE_NAME)
13521359

1360+
# Resume (does nothing if not suspended), so suspending doesn't prevent the cancel operation.
1361+
main_suspend(path_to_cookie=path_to_cookie, suspend=False, verbose=False)
1362+
13531363
touch(path_to_cookie)
13541364

13551365

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

1373+
# Resume (does nothing if not suspended), so suspending doesn't prevent the cancel operation.
1374+
main_suspend(path_to_cookie=path_to_cookie, suspend=False, verbose=False)
1375+
13631376
file_remove_if_exists(path_to_cookie)
13641377

13651378

1379+
def main_suspend(
1380+
*,
1381+
path_to_cookie: str = "",
1382+
suspend: bool,
1383+
verbose: bool,
1384+
) -> None:
1385+
import signal
1386+
1387+
if not path_to_cookie:
1388+
path_to_cookie = os.path.join(tempfile.gettempdir(), TEMP_COOKIE_NAME)
1389+
1390+
if not os.path.exists(path_to_cookie):
1391+
if verbose:
1392+
sys.stderr.write("No running nerd-dictation cookie found at: {:s}, abort!\n".format(path_to_cookie))
1393+
return
1394+
1395+
with open(path_to_cookie, "r", encoding="utf-8") as fh:
1396+
data = fh.read()
1397+
try:
1398+
pid = int(data)
1399+
except Exception as ex:
1400+
if verbose:
1401+
sys.stderr.write("Failed to read PID with error {!r}, abort!\n".format(ex))
1402+
return
1403+
1404+
if suspend:
1405+
os.kill(pid, signal.SIGUSR2)
1406+
else: # Resume.
1407+
os.kill(pid, signal.SIGCONT)
1408+
1409+
13661410
def argparse_generic_command_cookie(subparse: argparse.ArgumentParser) -> None:
13671411
subparse.add_argument(
13681412
"--cookie",
@@ -1696,15 +1740,67 @@ def argparse_create_cancel(subparsers: "argparse._SubParsersAction[argparse.Argu
16961740
)
16971741

16981742

1743+
def argparse_create_suspend(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
1744+
subparse = subparsers.add_parser(
1745+
"suspend",
1746+
help="Suspend the dictation process.",
1747+
description=(
1748+
"Suspend recording audio & the dictation process.\n"
1749+
"\n"
1750+
"This is useful on slower systems or when large language models take longer to load.\n"
1751+
"Recording audio is stopped and the process is paused to remove any CPU overhead."
1752+
),
1753+
formatter_class=argparse.RawTextHelpFormatter,
1754+
)
1755+
1756+
argparse_generic_command_cookie(subparse)
1757+
1758+
subparse.set_defaults(
1759+
func=lambda args: main_suspend(
1760+
path_to_cookie=args.path_to_cookie,
1761+
suspend=True,
1762+
verbose=True,
1763+
),
1764+
)
1765+
1766+
1767+
def argparse_create_resume(subparsers: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
1768+
subparse = subparsers.add_parser(
1769+
"resume",
1770+
help="Resume the dictation process.",
1771+
description=(
1772+
"Resume recording audio & the dictation process.\n"
1773+
"\n"
1774+
"This is to be used to resume after the 'suspend' command.\n"
1775+
"When nerd-dictation is not suspended, this does nothing.\n"
1776+
),
1777+
formatter_class=argparse.RawTextHelpFormatter,
1778+
)
1779+
1780+
argparse_generic_command_cookie(subparse)
1781+
1782+
subparse.set_defaults(
1783+
func=lambda args: main_suspend(
1784+
path_to_cookie=args.path_to_cookie,
1785+
suspend=False,
1786+
verbose=True,
1787+
),
1788+
)
1789+
1790+
16991791
def argparse_create() -> argparse.ArgumentParser:
17001792
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawTextHelpFormatter)
17011793

17021794
subparsers = parser.add_subparsers()
17031795

17041796
argparse_create_begin(subparsers)
1797+
17051798
argparse_create_end(subparsers)
17061799
argparse_create_cancel(subparsers)
17071800

1801+
argparse_create_suspend(subparsers)
1802+
argparse_create_resume(subparsers)
1803+
17081804
return parser
17091805

17101806

readme.rst

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,14 @@ While it could use any system currently it uses the VOSK-API.
180180

181181
positional arguments:
182182

183-
:begin: Begin dictation.
184-
:end: End dictation.
185-
:cancel: Cancel dictation.
183+
:begin: Begin dictation.
184+
:end: End dictation.
185+
:cancel: Cancel dictation.
186+
:suspend: Suspend the dictation process.
187+
:resume: Resume the dictation process.
186188

187189
options:
188-
-h, --help show this help message and exit
190+
-h, --help show this help message and exit
189191

190192
Subcommand: ``begin``
191193
---------------------
@@ -297,6 +299,38 @@ usage::
297299

298300
This cancels dictation.
299301

302+
options:
303+
-h, --help show this help message and exit
304+
--cookie FILE_PATH Location for writing a temporary cookie (this file is monitored to begin/end dictation).
305+
306+
Subcommand: ``suspend``
307+
-----------------------
308+
309+
usage::
310+
311+
nerd-dictation suspend [-h] [--cookie FILE_PATH]
312+
313+
Suspend recording audio & the dictation process.
314+
315+
This is useful on slower systems or when large language models take longer to load.
316+
Recording audio is stopped and the process is paused to remove any CPU overhead.
317+
318+
options:
319+
-h, --help show this help message and exit
320+
--cookie FILE_PATH Location for writing a temporary cookie (this file is monitored to begin/end dictation).
321+
322+
Subcommand: ``resume``
323+
----------------------
324+
325+
usage::
326+
327+
nerd-dictation resume [-h] [--cookie FILE_PATH]
328+
329+
Resume recording audio & the dictation process.
330+
331+
This is to be used to resume after the 'suspend' command.
332+
When nerd-dictation is not suspended, this does nothing.
333+
300334
options:
301335
-h, --help show this help message and exit
302336
--cookie FILE_PATH Location for writing a temporary cookie (this file is monitored to begin/end dictation).

0 commit comments

Comments
 (0)