stackvox 0.9.0__tar.gz → 0.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stackvox-0.9.0/stackvox.egg-info → stackvox-0.10.0}/PKG-INFO +1 -1
- {stackvox-0.9.0 → stackvox-0.10.0}/pyproject.toml +1 -1
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/cli.py +13 -1
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/daemon.py +25 -1
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/text.py +3 -2
- {stackvox-0.9.0 → stackvox-0.10.0/stackvox.egg-info}/PKG-INFO +1 -1
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_cli.py +14 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_daemon_protocol.py +8 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_text.py +7 -1
- {stackvox-0.9.0 → stackvox-0.10.0}/LICENSE +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/NOTICE +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/README.md +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/setup.cfg +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/__init__.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/__main__.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/config.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/data/stackvox-say +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/engine.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/paths.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/py.typed +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/updates.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/SOURCES.txt +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/dependency_links.txt +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/entry_points.txt +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/requires.txt +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/top_level.txt +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_config.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_daemon.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_engine.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_paths.py +0 -0
- {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_updates.py +0 -0
|
@@ -26,6 +26,7 @@ def _configure_logging() -> None:
|
|
|
26
26
|
SUBCOMMANDS = {
|
|
27
27
|
"serve",
|
|
28
28
|
"stop",
|
|
29
|
+
"cancel",
|
|
29
30
|
"status",
|
|
30
31
|
"say",
|
|
31
32
|
"speak",
|
|
@@ -51,7 +52,7 @@ _stackvox_completion() {
|
|
|
51
52
|
subcommand="${COMP_WORDS[1]:-}"
|
|
52
53
|
|
|
53
54
|
if [[ ${COMP_CWORD} -eq 1 ]]; then
|
|
54
|
-
COMPREPLY=( $(compgen -W "speak say normalize serve stop status voices welcome paths config completion install-helper" -- "$cur") )
|
|
55
|
+
COMPREPLY=( $(compgen -W "speak say normalize serve stop cancel status voices welcome paths config completion install-helper" -- "$cur") )
|
|
55
56
|
return 0
|
|
56
57
|
fi
|
|
57
58
|
|
|
@@ -155,6 +156,7 @@ def _build_parser(defaults: config.Defaults | None = None) -> argparse.ArgumentP
|
|
|
155
156
|
_add_voice_args(p_serve, defaults)
|
|
156
157
|
|
|
157
158
|
sub.add_parser("stop", help="Stop the running daemon")
|
|
159
|
+
sub.add_parser("cancel", help="Stop the current utterance without shutting down the daemon")
|
|
158
160
|
sub.add_parser("status", help="Print daemon status")
|
|
159
161
|
sub.add_parser("voices", help="List available voices")
|
|
160
162
|
sub.add_parser("welcome", help="Play a multilingual welcome message")
|
|
@@ -388,6 +390,15 @@ def _cmd_stop(_: argparse.Namespace) -> int:
|
|
|
388
390
|
return 0 if ok else 1
|
|
389
391
|
|
|
390
392
|
|
|
393
|
+
def _cmd_cancel(_: argparse.Namespace) -> int:
|
|
394
|
+
# Interrupt the current utterance but leave the daemon running (no model
|
|
395
|
+
# reload). Nothing playing / no daemon is not an error.
|
|
396
|
+
if not daemon.is_running():
|
|
397
|
+
return 0
|
|
398
|
+
ok, _resp = daemon.cancel()
|
|
399
|
+
return 0 if ok else 1
|
|
400
|
+
|
|
401
|
+
|
|
391
402
|
def _cmd_status(_: argparse.Namespace) -> int:
|
|
392
403
|
installed = updates._current_version()
|
|
393
404
|
if daemon.is_running():
|
|
@@ -530,6 +541,7 @@ def main() -> int:
|
|
|
530
541
|
"normalize": _cmd_normalize,
|
|
531
542
|
"serve": _cmd_serve,
|
|
532
543
|
"stop": _cmd_stop,
|
|
544
|
+
"cancel": _cmd_cancel,
|
|
533
545
|
"status": _cmd_status,
|
|
534
546
|
"voices": _cmd_voices,
|
|
535
547
|
"welcome": _cmd_welcome,
|
|
@@ -5,8 +5,10 @@ Protocol: one JSON object per connection (line-terminated), reply is a status li
|
|
|
5
5
|
|
|
6
6
|
Request shapes:
|
|
7
7
|
{"text": "...", "voice": "af_sarah", "speed": 1.0, "lang": "en-us"}
|
|
8
|
-
{"command": "stop"}
|
|
8
|
+
{"command": "stop"} # shut the daemon down
|
|
9
|
+
{"command": "cancel"} # stop the current utterance, keep the daemon up
|
|
9
10
|
{"command": "ping"}
|
|
11
|
+
{"command": "version"}
|
|
10
12
|
|
|
11
13
|
Replies: "ok", "busy", "err: <msg>".
|
|
12
14
|
"""
|
|
@@ -203,6 +205,17 @@ class _DaemonState:
|
|
|
203
205
|
def shutdown(self) -> None:
|
|
204
206
|
self.stop_event.set()
|
|
205
207
|
|
|
208
|
+
def cancel(self) -> None:
|
|
209
|
+
"""Stop the current utterance and drop anything queued, without shutting
|
|
210
|
+
the daemon down. Drains the queue first so the worker can't immediately
|
|
211
|
+
pick up a pending item, then aborts the in-flight playback."""
|
|
212
|
+
while True:
|
|
213
|
+
try:
|
|
214
|
+
self.queue.get_nowait()
|
|
215
|
+
except queue.Empty:
|
|
216
|
+
break
|
|
217
|
+
self.tts.stop()
|
|
218
|
+
|
|
206
219
|
|
|
207
220
|
class _Handler(socketserver.StreamRequestHandler):
|
|
208
221
|
def handle(self) -> None:
|
|
@@ -231,6 +244,11 @@ class _Handler(socketserver.StreamRequestHandler):
|
|
|
231
244
|
threading.Thread(target=self.server.shutdown, daemon=True).start()
|
|
232
245
|
return
|
|
233
246
|
|
|
247
|
+
if command == "cancel":
|
|
248
|
+
state.cancel()
|
|
249
|
+
self.wfile.write(b"ok\n")
|
|
250
|
+
return
|
|
251
|
+
|
|
234
252
|
text = req.get("text")
|
|
235
253
|
if not text:
|
|
236
254
|
self.wfile.write(b"err: missing text\n")
|
|
@@ -345,6 +363,12 @@ def stop() -> tuple[bool, str]:
|
|
|
345
363
|
return send({"command": "stop"})
|
|
346
364
|
|
|
347
365
|
|
|
366
|
+
def cancel() -> tuple[bool, str]:
|
|
367
|
+
"""Stop the daemon's current utterance (and drop the queue) without shutting
|
|
368
|
+
it down."""
|
|
369
|
+
return send({"command": "cancel"})
|
|
370
|
+
|
|
371
|
+
|
|
348
372
|
def ping() -> tuple[bool, str]:
|
|
349
373
|
return send({"command": "ping"}, timeout=PING_TIMEOUT_SECONDS)
|
|
350
374
|
|
|
@@ -133,14 +133,15 @@ _DEV_PRONUNCIATIONS: dict[str, str] = {
|
|
|
133
133
|
"cli": "C L I",
|
|
134
134
|
"ci": "C I",
|
|
135
135
|
"ide": "I D E",
|
|
136
|
-
"aws": "A
|
|
136
|
+
"aws": "A.W.S.", # dotted: espeak reads a lone "A" as the article "uh"; dots force the letter name "ay"
|
|
137
137
|
"uri": "U R I",
|
|
138
|
-
"iam": "I
|
|
138
|
+
"iam": "I.A.M.", # ditto — the middle "A" needs the dot
|
|
139
139
|
"saas": "sass",
|
|
140
140
|
"paas": "pass",
|
|
141
141
|
"tui": "T U I",
|
|
142
142
|
"postgresql": "postgres",
|
|
143
143
|
"kubectl": "kube control",
|
|
144
|
+
"stackone": "stack one", # org name espeak garbles as "stac kone"
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
|
|
@@ -523,3 +523,17 @@ class TestNormalizeParsing:
|
|
|
523
523
|
mocker.patch.object(cli.sys, "argv", ["stackvox", "paths"])
|
|
524
524
|
assert cli.main() == 0
|
|
525
525
|
assert paths_handler.called
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
class TestCmdCancel:
|
|
529
|
+
def test_running_calls_daemon_cancel(self, mocker):
|
|
530
|
+
mocker.patch.object(cli.daemon, "is_running", return_value=True)
|
|
531
|
+
cancel = mocker.patch.object(cli.daemon, "cancel", return_value=(True, "ok"))
|
|
532
|
+
assert cli._cmd_cancel(_ns()) == 0
|
|
533
|
+
cancel.assert_called_once()
|
|
534
|
+
|
|
535
|
+
def test_no_daemon_is_a_noop(self, mocker):
|
|
536
|
+
mocker.patch.object(cli.daemon, "is_running", return_value=False)
|
|
537
|
+
cancel = mocker.patch.object(cli.daemon, "cancel")
|
|
538
|
+
assert cli._cmd_cancel(_ns()) == 0
|
|
539
|
+
cancel.assert_not_called()
|
|
@@ -169,3 +169,11 @@ def test_send_helper_when_daemon_not_running(tmp_path, monkeypatch):
|
|
|
169
169
|
ok, resp = daemon.send({"command": "ping"})
|
|
170
170
|
assert not ok
|
|
171
171
|
assert resp == "daemon not running"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_cancel_stops_playback_and_keeps_daemon_up(server: ServerHarness):
|
|
175
|
+
reply = _roundtrip(server.sock, json.dumps({"command": "cancel"}) + "\n")
|
|
176
|
+
assert reply == "ok"
|
|
177
|
+
server.tts.stop.assert_called() # current utterance aborted
|
|
178
|
+
# unlike `stop`, the daemon stays up
|
|
179
|
+
assert _roundtrip(server.sock, json.dumps({"command": "ping"}) + "\n") == "ok"
|
|
@@ -226,11 +226,17 @@ def test_plain_text_mode_splits_paragraphs():
|
|
|
226
226
|
def test_dev_acronyms_are_spelled_out():
|
|
227
227
|
assert normalize_for_speech("Use the CLI.", markdown=False) == "Use the C L I."
|
|
228
228
|
assert "C L I" in normalize_for_speech("the cli tool", markdown=False) # lowercase too
|
|
229
|
-
assert "A
|
|
229
|
+
assert "A.W.S." in normalize_for_speech("deploy to AWS", markdown=False) # dotted → "ay"
|
|
230
|
+
assert "I.A.M." in normalize_for_speech("set up IAM", markdown=False)
|
|
230
231
|
assert "U R I" in normalize_for_speech("parse the URI", markdown=False)
|
|
231
232
|
assert "C I C D" in normalize_for_speech("the CI/CD pipeline", markdown=False)
|
|
232
233
|
|
|
233
234
|
|
|
235
|
+
def test_org_names_are_spoken_word_by_word():
|
|
236
|
+
assert "stack one" in normalize_for_speech("deployed to StackOne today", markdown=False)
|
|
237
|
+
assert "stack one" in normalize_for_speech("the stackone api", markdown=False) # case-insensitive
|
|
238
|
+
|
|
239
|
+
|
|
234
240
|
def test_dev_terms_leave_correctly_voiced_acronyms_alone():
|
|
235
241
|
out = normalize_for_speech("Send JSON over HTTP to the API.", markdown=False)
|
|
236
242
|
assert "JSON" in out
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|