stackvox 0.10.0__tar.gz → 0.12.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 (32) hide show
  1. {stackvox-0.10.0/stackvox.egg-info → stackvox-0.12.0}/PKG-INFO +6 -2
  2. {stackvox-0.10.0 → stackvox-0.12.0}/README.md +5 -1
  3. {stackvox-0.10.0 → stackvox-0.12.0}/pyproject.toml +9 -1
  4. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/cli.py +40 -5
  5. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/daemon.py +39 -8
  6. stackvox-0.12.0/stackvox/text.py +738 -0
  7. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/updates.py +39 -1
  8. {stackvox-0.10.0 → stackvox-0.12.0/stackvox.egg-info}/PKG-INFO +6 -2
  9. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_cli.py +44 -0
  10. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_daemon.py +72 -0
  11. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_text.py +235 -0
  12. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_updates.py +45 -0
  13. stackvox-0.10.0/stackvox/text.py +0 -356
  14. {stackvox-0.10.0 → stackvox-0.12.0}/LICENSE +0 -0
  15. {stackvox-0.10.0 → stackvox-0.12.0}/NOTICE +0 -0
  16. {stackvox-0.10.0 → stackvox-0.12.0}/setup.cfg +0 -0
  17. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/__init__.py +0 -0
  18. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/__main__.py +0 -0
  19. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/config.py +0 -0
  20. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/data/stackvox-say +0 -0
  21. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/engine.py +0 -0
  22. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/paths.py +0 -0
  23. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox/py.typed +0 -0
  24. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox.egg-info/SOURCES.txt +0 -0
  25. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox.egg-info/dependency_links.txt +0 -0
  26. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox.egg-info/entry_points.txt +0 -0
  27. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox.egg-info/requires.txt +0 -0
  28. {stackvox-0.10.0 → stackvox-0.12.0}/stackvox.egg-info/top_level.txt +0 -0
  29. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_config.py +0 -0
  30. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_daemon_protocol.py +0 -0
  31. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_engine.py +0 -0
  32. {stackvox-0.10.0 → stackvox-0.12.0}/tests/test_paths.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackvox
3
- Version: 0.10.0
3
+ Version: 0.12.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
@@ -103,7 +103,11 @@ stackvox speak --normalize --file post.md # normalize, then synthesize
103
103
  stackvox say --normalize "**Shipped** 1,198.9 MPG" # normalize, then send to daemon
104
104
  ```
105
105
 
106
- `--normalize` is off by default on `speak`/`say`, so notification phrases synthesize exactly as before. The tuning flags mirror the library defaults (all on except emoji-strip): `--no-markdown`, `--no-expand-units`, `--no-expand-numbers`, `--no-pauses`, `--tables {drop,csv}`, `--strip-emoji`, `--no-terminal-stops`, `--locale`, and a `--pronunciations FILE` (a JSON `{"written": "spoken"}` map, applied whole-word and case-insensitively).
106
+ `--normalize` is off by default on `speak`/`say`, so notification phrases synthesize exactly as before. The tuning flags mirror the library defaults (all on except emoji-strip): `--no-markdown`, `--no-dev-terms`, `--no-expand-units`, `--no-expand-numbers`, `--no-pauses`, `--tables {drop,csv}`, `--code-blocks {drop,placeholder}`, `--strip-emoji`, `--no-terminal-stops`, `--locale`, and a `--pronunciations FILE` (a JSON `{"written": "spoken"}` map, applied whole-word and case-insensitively).
107
+
108
+ **Dev-output handling** (on by default; `--no-dev-terms` to disable) covers two things espeak reads badly: acronyms it says as words (`CLI` → "C L I", `AWS`, `URI`, `IAM`, …) and file-line references, which it voices literally as "dot py colon". A `file.ext:line` ref is re-spoken lead-with-the-line: `engine.py:42` → "line 42 of engine py", `cli.py:100-118` → "lines 100 to 118 of cli py", `foo.ts:666:10` → "line 666, column 10 of foo ts". Times, ratios, and versions (`12:30`, `3:1`, `1.2.3`) are left alone.
109
+
110
+ Fenced code blocks are dropped by default. `--code-blocks placeholder` instead speaks a short stand-in so a skipped block doesn't sound disjointed (ChatGPT-style) — set the wording with `--code-placeholder` (e.g. `--code-blocks placeholder --code-placeholder "you can see the code in the chat history"`); consecutive blocks collapse to one.
107
111
 
108
112
  Bash completion:
109
113
 
@@ -69,7 +69,11 @@ stackvox speak --normalize --file post.md # normalize, then synthesize
69
69
  stackvox say --normalize "**Shipped** 1,198.9 MPG" # normalize, then send to daemon
70
70
  ```
71
71
 
72
- `--normalize` is off by default on `speak`/`say`, so notification phrases synthesize exactly as before. The tuning flags mirror the library defaults (all on except emoji-strip): `--no-markdown`, `--no-expand-units`, `--no-expand-numbers`, `--no-pauses`, `--tables {drop,csv}`, `--strip-emoji`, `--no-terminal-stops`, `--locale`, and a `--pronunciations FILE` (a JSON `{"written": "spoken"}` map, applied whole-word and case-insensitively).
72
+ `--normalize` is off by default on `speak`/`say`, so notification phrases synthesize exactly as before. The tuning flags mirror the library defaults (all on except emoji-strip): `--no-markdown`, `--no-dev-terms`, `--no-expand-units`, `--no-expand-numbers`, `--no-pauses`, `--tables {drop,csv}`, `--code-blocks {drop,placeholder}`, `--strip-emoji`, `--no-terminal-stops`, `--locale`, and a `--pronunciations FILE` (a JSON `{"written": "spoken"}` map, applied whole-word and case-insensitively).
73
+
74
+ **Dev-output handling** (on by default; `--no-dev-terms` to disable) covers two things espeak reads badly: acronyms it says as words (`CLI` → "C L I", `AWS`, `URI`, `IAM`, …) and file-line references, which it voices literally as "dot py colon". A `file.ext:line` ref is re-spoken lead-with-the-line: `engine.py:42` → "line 42 of engine py", `cli.py:100-118` → "lines 100 to 118 of cli py", `foo.ts:666:10` → "line 666, column 10 of foo ts". Times, ratios, and versions (`12:30`, `3:1`, `1.2.3`) are left alone.
75
+
76
+ Fenced code blocks are dropped by default. `--code-blocks placeholder` instead speaks a short stand-in so a skipped block doesn't sound disjointed (ChatGPT-style) — set the wording with `--code-placeholder` (e.g. `--code-blocks placeholder --code-placeholder "you can see the code in the chat history"`); consecutive blocks collapse to one.
73
77
 
74
78
  Bash completion:
75
79
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stackvox"
7
- version = "0.10.0"
7
+ version = "0.12.0"
8
8
  description = "Offline TTS library using Kokoro-82M"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -61,6 +61,14 @@ stackvox = ["py.typed", "data/stackvox-say"]
61
61
  line-length = 110
62
62
  target-version = "py310"
63
63
 
64
+ [tool.ruff.format]
65
+ # Ruff 0.16 began formatting Python code blocks inside Markdown. Ours are
66
+ # illustrative rather than real code, and their layout is deliberate: the
67
+ # `normalize_for_speech` signature in docs/speech-normalization.md aligns its
68
+ # trailing comments into a readable column of flags, which the formatter
69
+ # collapses. Prose is a human call, so keep the formatter to the code.
70
+ exclude = ["*.md"]
71
+
64
72
  [tool.ruff.lint]
65
73
  select = [
66
74
  "E", # pycodestyle errors
@@ -77,13 +77,17 @@ _stackvox_completion() {
77
77
  COMPREPLY=( $(compgen -W "drop csv" -- "$cur") )
78
78
  return 0
79
79
  ;;
80
+ --code-blocks)
81
+ COMPREPLY=( $(compgen -W "drop placeholder" -- "$cur") )
82
+ return 0
83
+ ;;
80
84
  --locale)
81
85
  COMPREPLY=( $(compgen -W "en-GB" -- "$cur") )
82
86
  return 0
83
87
  ;;
84
88
  esac
85
89
 
86
- local norm_flags="--no-markdown --pronunciations --no-expand-units --no-expand-numbers --no-pauses --tables --strip-emoji --no-terminal-stops --locale"
90
+ local norm_flags="--no-markdown --no-dev-terms --no-filenames --pronunciations --no-expand-units --no-expand-numbers --no-pauses --tables --code-blocks --code-placeholder --strip-emoji --no-terminal-stops --locale"
87
91
 
88
92
  case "$subcommand" in
89
93
  speak)
@@ -216,6 +220,12 @@ def _add_normalize_args(parser: argparse.ArgumentParser, *, with_switch: bool) -
216
220
  action="store_false",
217
221
  help="Do not spell out dev acronyms espeak mispronounces (CLI, CI, IDE, AWS, URI, IAM, ...)",
218
222
  )
223
+ parser.add_argument(
224
+ "--no-filenames",
225
+ dest="filenames",
226
+ action="store_false",
227
+ help="Leave filenames and paths as written (no 'README dot md', no 'line 42 of cli dot py')",
228
+ )
219
229
  parser.add_argument(
220
230
  "--no-expand-units",
221
231
  dest="expand_units",
@@ -240,6 +250,19 @@ def _add_normalize_args(parser: argparse.ArgumentParser, *, with_switch: bool) -
240
250
  default="drop",
241
251
  help="How Markdown tables are voiced (default: drop)",
242
252
  )
253
+ parser.add_argument(
254
+ "--code-blocks",
255
+ dest="code_blocks",
256
+ choices=["drop", "placeholder"],
257
+ default="drop",
258
+ help="How fenced code blocks are voiced (default: drop)",
259
+ )
260
+ parser.add_argument(
261
+ "--code-placeholder",
262
+ dest="code_placeholder",
263
+ default="Code block.",
264
+ help='Spoken stand-in for a code block when --code-blocks placeholder (default: "Code block.")',
265
+ )
243
266
  parser.add_argument(
244
267
  "--strip-emoji", dest="strip_emoji", action="store_true", help="Remove emoji before speaking"
245
268
  )
@@ -277,10 +300,13 @@ def _normalize_kwargs(args: argparse.Namespace) -> dict:
277
300
  "markdown": args.markdown,
278
301
  "pronunciations": _load_pronunciations(args.pronunciations),
279
302
  "dev_terms": args.dev_terms,
303
+ "filenames": args.filenames,
280
304
  "expand_units": args.expand_units,
281
305
  "expand_numbers": args.expand_numbers,
282
306
  "pauses": args.pauses,
283
307
  "tables": args.tables,
308
+ "code_blocks": args.code_blocks,
309
+ "code_placeholder": args.code_placeholder,
284
310
  "strip_emoji": args.strip_emoji,
285
311
  "terminal_stops": args.terminal_stops,
286
312
  "locale": args.locale,
@@ -406,12 +432,21 @@ def _cmd_status(_: argparse.Namespace) -> int:
406
432
  rc = 0
407
433
  # The running daemon can lag the installed package if it wasn't restarted
408
434
  # after an upgrade — surface that skew instead of trusting it blindly.
435
+ # Only a daemon that is genuinely BEHIND wants restarting; a newer one
436
+ # means the client just run is the older install (a checkout alongside a
437
+ # global one), and restarting would downgrade what's serving.
409
438
  got, running_version = daemon.version()
410
439
  if got and running_version != installed:
411
- print(
412
- f"daemon running {running_version}, but {installed} is installed "
413
- f"— restart the daemon (stackvox stop; stackvox serve) to pick it up"
414
- )
440
+ if updates._is_newer(installed, running_version):
441
+ print(
442
+ f"daemon running {running_version}, but {installed} is installed "
443
+ f"— restart the daemon (stackvox stop; stackvox serve) to pick it up"
444
+ )
445
+ else:
446
+ print(
447
+ f"daemon running {running_version} is newer than the {installed} client "
448
+ f"you just ran; no restart needed"
449
+ )
415
450
  else:
416
451
  print("stopped")
417
452
  rc = 1
@@ -26,6 +26,7 @@ import socket
26
26
  import socketserver
27
27
  import sys
28
28
  import threading
29
+ import time
29
30
  from typing import Any
30
31
 
31
32
  import sounddevice as sd
@@ -42,6 +43,10 @@ MAX_QUEUE = 2
42
43
  WORKER_POLL_SECONDS = 0.5
43
44
  CLIENT_TIMEOUT_SECONDS = 1.0
44
45
  PING_TIMEOUT_SECONDS = 0.5
46
+ # How long `stop` waits for the daemon process to actually exit after it has
47
+ # acknowledged the request, and how often it re-checks.
48
+ STOP_TIMEOUT_SECONDS = 5.0
49
+ STOP_POLL_SECONDS = 0.05
45
50
  RECV_BYTES = 1024
46
51
 
47
52
 
@@ -273,14 +278,19 @@ def _pid_alive(pid: int) -> bool:
273
278
  return False
274
279
 
275
280
 
276
- def is_running() -> bool:
281
+ def _read_pid() -> int | None:
282
+ """The pid recorded in the pid file, or None if absent or unparseable."""
277
283
  if not PID_PATH.exists():
278
- return False
284
+ return None
279
285
  try:
280
- pid = int(PID_PATH.read_text().strip())
281
- except ValueError:
282
- return False
283
- return _pid_alive(pid)
286
+ return int(PID_PATH.read_text().strip())
287
+ except (ValueError, OSError):
288
+ return None
289
+
290
+
291
+ def is_running() -> bool:
292
+ pid = _read_pid()
293
+ return pid is not None and _pid_alive(pid)
284
294
 
285
295
 
286
296
  def _check_for_update_async() -> None:
@@ -359,8 +369,29 @@ def say(
359
369
  return send(req)
360
370
 
361
371
 
362
- def stop() -> tuple[bool, str]:
363
- return send({"command": "stop"})
372
+ def stop(timeout: float = STOP_TIMEOUT_SECONDS) -> tuple[bool, str]:
373
+ """Shut the daemon down and wait until the process has actually gone.
374
+
375
+ The daemon acknowledges the request and only then unwinds `serve_forever`
376
+ and removes its pid file, so returning on the ack alone made
377
+ `stackvox stop && stackvox serve` race: `serve` still saw a live pid, refused
378
+ to start, and the shutdown then completed, leaving nothing running at all.
379
+ Reporting success before the process has exited makes that sequence, the one
380
+ `status` recommends, silently useless.
381
+ """
382
+ pid = _read_pid()
383
+ ok, resp = send({"command": "stop"})
384
+ if not ok or pid is None:
385
+ # Nothing acknowledged, or no pid to watch: the caller's own
386
+ # `is_running` check is as good as it gets.
387
+ return ok, resp
388
+ deadline = time.monotonic() + timeout
389
+ while True:
390
+ if not _pid_alive(pid):
391
+ return True, resp
392
+ if time.monotonic() >= deadline:
393
+ return False, f"daemon acknowledged stop but pid {pid} is still alive after {timeout:g}s"
394
+ time.sleep(STOP_POLL_SECONDS)
364
395
 
365
396
 
366
397
  def cancel() -> tuple[bool, str]: