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.
Files changed (31) hide show
  1. {stackvox-0.9.0/stackvox.egg-info → stackvox-0.10.0}/PKG-INFO +1 -1
  2. {stackvox-0.9.0 → stackvox-0.10.0}/pyproject.toml +1 -1
  3. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/cli.py +13 -1
  4. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/daemon.py +25 -1
  5. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/text.py +3 -2
  6. {stackvox-0.9.0 → stackvox-0.10.0/stackvox.egg-info}/PKG-INFO +1 -1
  7. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_cli.py +14 -0
  8. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_daemon_protocol.py +8 -0
  9. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_text.py +7 -1
  10. {stackvox-0.9.0 → stackvox-0.10.0}/LICENSE +0 -0
  11. {stackvox-0.9.0 → stackvox-0.10.0}/NOTICE +0 -0
  12. {stackvox-0.9.0 → stackvox-0.10.0}/README.md +0 -0
  13. {stackvox-0.9.0 → stackvox-0.10.0}/setup.cfg +0 -0
  14. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/__init__.py +0 -0
  15. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/__main__.py +0 -0
  16. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/config.py +0 -0
  17. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/data/stackvox-say +0 -0
  18. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/engine.py +0 -0
  19. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/paths.py +0 -0
  20. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/py.typed +0 -0
  21. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox/updates.py +0 -0
  22. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/SOURCES.txt +0 -0
  23. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/dependency_links.txt +0 -0
  24. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/entry_points.txt +0 -0
  25. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/requires.txt +0 -0
  26. {stackvox-0.9.0 → stackvox-0.10.0}/stackvox.egg-info/top_level.txt +0 -0
  27. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_config.py +0 -0
  28. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_daemon.py +0 -0
  29. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_engine.py +0 -0
  30. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_paths.py +0 -0
  31. {stackvox-0.9.0 → stackvox-0.10.0}/tests/test_updates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackvox
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Offline TTS library using Kokoro-82M
5
5
  Author-email: "StackOne Technologies Ltd." <support@stackone.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stackvox"
7
- version = "0.9.0"
7
+ version = "0.10.0"
8
8
  description = "Offline TTS library using Kokoro-82M"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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 W S",
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 A M",
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackvox
3
- Version: 0.9.0
3
+ Version: 0.10.0
4
4
  Summary: Offline TTS library using Kokoro-82M
5
5
  Author-email: "StackOne Technologies Ltd." <support@stackone.com>
6
6
  License-Expression: Apache-2.0
@@ -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 W S" in normalize_for_speech("deploy to AWS", markdown=False)
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