stackvox 0.8.0__tar.gz → 0.9.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.8.0/stackvox.egg-info → stackvox-0.9.0}/PKG-INFO +1 -1
  2. {stackvox-0.8.0 → stackvox-0.9.0}/pyproject.toml +1 -1
  3. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/cli.py +9 -0
  4. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/daemon.py +15 -0
  5. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/text.py +17 -0
  6. {stackvox-0.8.0 → stackvox-0.9.0/stackvox.egg-info}/PKG-INFO +1 -1
  7. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_cli.py +20 -0
  8. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_daemon_protocol.py +16 -0
  9. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_text.py +26 -0
  10. {stackvox-0.8.0 → stackvox-0.9.0}/LICENSE +0 -0
  11. {stackvox-0.8.0 → stackvox-0.9.0}/NOTICE +0 -0
  12. {stackvox-0.8.0 → stackvox-0.9.0}/README.md +0 -0
  13. {stackvox-0.8.0 → stackvox-0.9.0}/setup.cfg +0 -0
  14. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/__init__.py +0 -0
  15. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/__main__.py +0 -0
  16. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/config.py +0 -0
  17. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/data/stackvox-say +0 -0
  18. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/engine.py +0 -0
  19. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/paths.py +0 -0
  20. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/py.typed +0 -0
  21. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox/updates.py +0 -0
  22. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox.egg-info/SOURCES.txt +0 -0
  23. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox.egg-info/dependency_links.txt +0 -0
  24. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox.egg-info/entry_points.txt +0 -0
  25. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox.egg-info/requires.txt +0 -0
  26. {stackvox-0.8.0 → stackvox-0.9.0}/stackvox.egg-info/top_level.txt +0 -0
  27. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_config.py +0 -0
  28. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_daemon.py +0 -0
  29. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_engine.py +0 -0
  30. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_paths.py +0 -0
  31. {stackvox-0.8.0 → stackvox-0.9.0}/tests/test_updates.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackvox
3
- Version: 0.8.0
3
+ Version: 0.9.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.8.0"
7
+ version = "0.9.0"
8
8
  description = "Offline TTS library using Kokoro-82M"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -389,9 +389,18 @@ def _cmd_stop(_: argparse.Namespace) -> int:
389
389
 
390
390
 
391
391
  def _cmd_status(_: argparse.Namespace) -> int:
392
+ installed = updates._current_version()
392
393
  if daemon.is_running():
393
394
  print(f"running (pid {daemon.PID_PATH.read_text().strip()}) on {daemon.SOCKET_PATH}")
394
395
  rc = 0
396
+ # The running daemon can lag the installed package if it wasn't restarted
397
+ # after an upgrade — surface that skew instead of trusting it blindly.
398
+ got, running_version = daemon.version()
399
+ if got and running_version != installed:
400
+ print(
401
+ f"daemon running {running_version}, but {installed} is installed "
402
+ f"— restart the daemon (stackvox stop; stackvox serve) to pick it up"
403
+ )
395
404
  else:
396
405
  print("stopped")
397
406
  rc = 1
@@ -222,6 +222,10 @@ class _Handler(socketserver.StreamRequestHandler):
222
222
  self.wfile.write(b"ok\n")
223
223
  return
224
224
 
225
+ if command == "version":
226
+ self.wfile.write(f"{updates._current_version()}\n".encode())
227
+ return
228
+
225
229
  if command == "stop":
226
230
  self.wfile.write(b"ok\n")
227
231
  threading.Thread(target=self.server.shutdown, daemon=True).start()
@@ -343,3 +347,14 @@ def stop() -> tuple[bool, str]:
343
347
 
344
348
  def ping() -> tuple[bool, str]:
345
349
  return send({"command": "ping"}, timeout=PING_TIMEOUT_SECONDS)
350
+
351
+
352
+ def version(timeout: float = PING_TIMEOUT_SECONDS) -> tuple[bool, str]:
353
+ """(got_version, version_or_error) reported by the running daemon.
354
+
355
+ The running daemon can differ from the installed package if it wasn't
356
+ restarted after an upgrade, so this reports what is actually serving — not
357
+ what's on disk. A version reply starts with a digit; error strings don't.
358
+ """
359
+ _, resp = send({"command": "version"}, timeout=timeout)
360
+ return resp[:1].isdigit(), resp
@@ -18,6 +18,7 @@ __all__ = [
18
18
  "markdown_to_paragraphs",
19
19
  "strip_emoji",
20
20
  "strip_thousands_separators",
21
+ "versions_to_words",
21
22
  "decimals_to_words",
22
23
  "expand_units",
23
24
  "apply_pronunciations",
@@ -84,6 +85,21 @@ def strip_thousands_separators(text: str) -> str:
84
85
  return re.sub(r"(?<=\d),(?=\d)", "", text)
85
86
 
86
87
 
88
+ _VERSION = re.compile(r"(?<!\d)\d+(?:\.\d+){2,}(?!\d)")
89
+
90
+
91
+ def versions_to_words(text: str) -> str:
92
+ """1.2.3 -> "1 point 2 point 3"; 0.7.0 -> "0 point 7 point 0".
93
+
94
+ A dotted version string has more than one decimal point, which
95
+ `decimals_to_words` can't handle — it splits only the first, leaving a stray
96
+ full stop mid-number ("0.7.0" -> "0 point 7.0"). Run this before it. Two-part
97
+ numbers are left untouched so ordinary decimals still read digit-by-digit,
98
+ and a trailing sentence stop (``upgrade to 1.2.3.``) is preserved.
99
+ """
100
+ return _VERSION.sub(lambda m: m.group(0).replace(".", " point "), text)
101
+
102
+
87
103
  def decimals_to_words(text: str) -> str:
88
104
  """1198.9 -> "1198 point 9"; 770.72 -> "770 point 7 2". Removes the bare
89
105
  "." between digits, which TTS can otherwise read as a full stop."""
@@ -287,6 +303,7 @@ def _shape_paragraph(
287
303
  if expand_units_flag: # units BEFORE decimals (see note above)
288
304
  text = expand_units(text, locale)
289
305
  if expand_numbers_flag:
306
+ text = versions_to_words(text) # multi-dot versions BEFORE the decimal split
290
307
  text = decimals_to_words(text)
291
308
  return re.sub(r"[ \t]{2,}", " ", text).strip()
292
309
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackvox
3
- Version: 0.8.0
3
+ Version: 0.9.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
@@ -261,6 +261,9 @@ class TestCmdStatus:
261
261
  pid_path.read_text.return_value = "12345\n"
262
262
  mocker.patch.object(cli.daemon, "PID_PATH", pid_path)
263
263
  mocker.patch.object(cli.daemon, "SOCKET_PATH", "/tmp/x.sock")
264
+ # status now queries the running daemon's version; stub it so the test
265
+ # doesn't reach a real socket.
266
+ mocker.patch.object(cli.daemon, "version", return_value=(False, "n/a"))
264
267
 
265
268
  rc = cli._cmd_status(_ns())
266
269
 
@@ -269,6 +272,23 @@ class TestCmdStatus:
269
272
  assert "running" in out
270
273
  assert "12345" in out
271
274
 
275
+ def test_running_flags_daemon_version_skew(self, mocker, capsys):
276
+ mocker.patch.object(cli.daemon, "is_running", return_value=True)
277
+ pid_path = mocker.MagicMock()
278
+ pid_path.read_text.return_value = "1\n"
279
+ mocker.patch.object(cli.daemon, "PID_PATH", pid_path)
280
+ mocker.patch.object(cli.daemon, "SOCKET_PATH", "/tmp/x.sock")
281
+ mocker.patch.object(cli.daemon, "version", return_value=(True, "0.5.0"))
282
+ mocker.patch.object(cli.updates, "_current_version", return_value="0.8.0")
283
+ mocker.patch.object(cli.updates, "check_for_update", return_value=None)
284
+
285
+ rc = cli._cmd_status(_ns())
286
+
287
+ assert rc == 0
288
+ out = capsys.readouterr().out
289
+ assert "0.5.0" in out and "0.8.0" in out # the skew is surfaced
290
+ assert "restart" in out.lower()
291
+
272
292
  def test_stopped_returns_one(self, mocker, capsys):
273
293
  mocker.patch.object(cli.daemon, "is_running", return_value=False)
274
294
  rc = cli._cmd_status(_ns())
@@ -76,6 +76,22 @@ def test_ping_returns_ok(server: ServerHarness):
76
76
  assert _roundtrip(server.sock, json.dumps({"command": "ping"}) + "\n") == "ok"
77
77
 
78
78
 
79
+ def test_version_returns_installed_version(server: ServerHarness):
80
+ from stackvox import updates
81
+
82
+ reply = _roundtrip(server.sock, json.dumps({"command": "version"}) + "\n")
83
+ assert reply == updates._current_version()
84
+
85
+
86
+ def test_version_client_helper_reports_running_version(server: ServerHarness, monkeypatch):
87
+ from stackvox import daemon, updates
88
+
89
+ monkeypatch.setattr(daemon, "SOCKET_PATH", server.sock)
90
+ got, resp = daemon.version()
91
+ assert got is True
92
+ assert resp == updates._current_version()
93
+
94
+
79
95
  def test_plain_text_is_treated_as_text_field(server: ServerHarness):
80
96
  """Non-JSON payloads are accepted and wrapped as `{"text": line}`."""
81
97
  import time
@@ -10,6 +10,7 @@ from stackvox.text import (
10
10
  shape_pauses,
11
11
  strip_emoji,
12
12
  strip_thousands_separators,
13
+ versions_to_words,
13
14
  )
14
15
 
15
16
  # --- numbers ---------------------------------------------------------------
@@ -28,6 +29,31 @@ def test_year_is_not_a_decimal():
28
29
  assert decimals_to_words("built in 2023") == "built in 2023"
29
30
 
30
31
 
32
+ def test_semver_all_parts_spoken():
33
+ assert versions_to_words("0.7.0") == "0 point 7 point 0"
34
+ assert versions_to_words("1.2.3") == "1 point 2 point 3"
35
+
36
+
37
+ def test_semver_multi_digit_component():
38
+ assert versions_to_words("1.20.3") == "1 point 20 point 3"
39
+
40
+
41
+ def test_version_preserves_trailing_sentence_stop():
42
+ assert versions_to_words("upgrade to 1.2.3.") == "upgrade to 1 point 2 point 3."
43
+
44
+
45
+ def test_two_part_number_is_left_for_the_decimal_pass():
46
+ # Only two components — an ordinary decimal, not a version.
47
+ assert versions_to_words("3.14") == "3.14"
48
+ assert decimals_to_words(versions_to_words("3.14")) == "3 point 1 4"
49
+
50
+
51
+ def test_semver_normalizes_end_to_end():
52
+ out = normalize_for_speech("Upgraded to 0.7.0 today.", markdown=False)
53
+ assert "0 point 7 point 0" in out
54
+ assert "0 point 7.0" not in out
55
+
56
+
31
57
  # --- units -----------------------------------------------------------------
32
58
 
33
59
 
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