stackvox 0.10.0__tar.gz → 0.11.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.10.0/stackvox.egg-info → stackvox-0.11.0}/PKG-INFO +6 -2
- {stackvox-0.10.0 → stackvox-0.11.0}/README.md +5 -1
- {stackvox-0.10.0 → stackvox-0.11.0}/pyproject.toml +1 -1
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/cli.py +20 -1
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/text.py +89 -4
- {stackvox-0.10.0 → stackvox-0.11.0/stackvox.egg-info}/PKG-INFO +6 -2
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_cli.py +22 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_text.py +88 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/LICENSE +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/NOTICE +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/setup.cfg +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/__init__.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/__main__.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/config.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/daemon.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/data/stackvox-say +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/engine.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/paths.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/py.typed +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox/updates.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox.egg-info/SOURCES.txt +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox.egg-info/dependency_links.txt +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox.egg-info/entry_points.txt +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox.egg-info/requires.txt +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/stackvox.egg-info/top_level.txt +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_config.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_daemon.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_daemon_protocol.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_engine.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_paths.py +0 -0
- {stackvox-0.10.0 → stackvox-0.11.0}/tests/test_updates.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackvox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
|
@@ -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 --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)
|
|
@@ -240,6 +244,19 @@ def _add_normalize_args(parser: argparse.ArgumentParser, *, with_switch: bool) -
|
|
|
240
244
|
default="drop",
|
|
241
245
|
help="How Markdown tables are voiced (default: drop)",
|
|
242
246
|
)
|
|
247
|
+
parser.add_argument(
|
|
248
|
+
"--code-blocks",
|
|
249
|
+
dest="code_blocks",
|
|
250
|
+
choices=["drop", "placeholder"],
|
|
251
|
+
default="drop",
|
|
252
|
+
help="How fenced code blocks are voiced (default: drop)",
|
|
253
|
+
)
|
|
254
|
+
parser.add_argument(
|
|
255
|
+
"--code-placeholder",
|
|
256
|
+
dest="code_placeholder",
|
|
257
|
+
default="Code block.",
|
|
258
|
+
help='Spoken stand-in for a code block when --code-blocks placeholder (default: "Code block.")',
|
|
259
|
+
)
|
|
243
260
|
parser.add_argument(
|
|
244
261
|
"--strip-emoji", dest="strip_emoji", action="store_true", help="Remove emoji before speaking"
|
|
245
262
|
)
|
|
@@ -281,6 +298,8 @@ def _normalize_kwargs(args: argparse.Namespace) -> dict:
|
|
|
281
298
|
"expand_numbers": args.expand_numbers,
|
|
282
299
|
"pauses": args.pauses,
|
|
283
300
|
"tables": args.tables,
|
|
301
|
+
"code_blocks": args.code_blocks,
|
|
302
|
+
"code_placeholder": args.code_placeholder,
|
|
284
303
|
"strip_emoji": args.strip_emoji,
|
|
285
304
|
"terminal_stops": args.terminal_stops,
|
|
286
305
|
"locale": args.locale,
|
|
@@ -20,6 +20,7 @@ __all__ = [
|
|
|
20
20
|
"strip_thousands_separators",
|
|
21
21
|
"versions_to_words",
|
|
22
22
|
"decimals_to_words",
|
|
23
|
+
"speak_file_refs",
|
|
23
24
|
"expand_units",
|
|
24
25
|
"apply_pronunciations",
|
|
25
26
|
"shape_pauses",
|
|
@@ -64,6 +65,7 @@ _UNIT_RULES: dict[str, list[tuple[str, str]]] = {
|
|
|
64
65
|
(r"\s*÷\s*", " divided by "),
|
|
65
66
|
(r"\s*×\s*", " times "),
|
|
66
67
|
(r"\s*=\s*", " equals "),
|
|
68
|
+
(r"~\s*(?=\d)", "about "), # ~123 -> "about 123" (else espeak says "tilde 123"); leaves ~/path alone
|
|
67
69
|
],
|
|
68
70
|
}
|
|
69
71
|
DEFAULT_LOCALE = "en-GB"
|
|
@@ -110,6 +112,46 @@ def decimals_to_words(text: str) -> str:
|
|
|
110
112
|
)
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
# --------------------------------------------------------------------------- #
|
|
116
|
+
# File & line references #
|
|
117
|
+
# --------------------------------------------------------------------------- #
|
|
118
|
+
# `engine.py:42` reads as "…dot py colon forty two" — the dots and colon get
|
|
119
|
+
# voiced literally. Spoken aloud the line number is the signal and the filename
|
|
120
|
+
# is noise, so lead with "line N of <file>" (how a person actually says it) and
|
|
121
|
+
# soften the path: drop directories to the basename, and voice the dotted name
|
|
122
|
+
# word by word.
|
|
123
|
+
|
|
124
|
+
_FILE_REF = re.compile(
|
|
125
|
+
r"(?<!\w)"
|
|
126
|
+
r"(?:[\w.-]*/)*" # optional directory segments (dropped — "src/" reads as "slash")
|
|
127
|
+
r"([A-Za-z0-9_-]+(?:\.[A-Za-z][\w-]*)+)" # filename with >=1 letter-initial extension
|
|
128
|
+
r":(\d+)(?:-(\d+))?(?::(\d+))?" # :line, optional -end (range) or :column
|
|
129
|
+
r"(?!\w)"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def speak_file_refs(text: str) -> str:
|
|
134
|
+
"""Turn ``path/file.ext:line`` refs into spoken "line N of file ext".
|
|
135
|
+
|
|
136
|
+
``engine.py:42`` -> "line 42 of engine py"; ``cli.py:100-118`` -> "lines 100
|
|
137
|
+
to 118 of cli py"; ``foo.ts:666:10`` -> "line 666, column 10 of foo ts". The
|
|
138
|
+
``:line`` suffix is the trigger, so bare times/ratios/verses (``12:30``,
|
|
139
|
+
``3:1``, ``John 3:16``) and dotted versions (``1.2.3``) are left untouched —
|
|
140
|
+
none of them carry a dotted-filename before the colon.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
def repl(match: re.Match[str]) -> str:
|
|
144
|
+
basename = match.group(1).replace(".", " ")
|
|
145
|
+
start, end, column = match.group(2), match.group(3), match.group(4)
|
|
146
|
+
if end:
|
|
147
|
+
location = f"lines {start} to {end}"
|
|
148
|
+
else:
|
|
149
|
+
location = f"line {start}" + (f", column {column}" if column else "")
|
|
150
|
+
return f"{location} of {basename}"
|
|
151
|
+
|
|
152
|
+
return _FILE_REF.sub(repl, text)
|
|
153
|
+
|
|
154
|
+
|
|
113
155
|
# --------------------------------------------------------------------------- #
|
|
114
156
|
# Pronunciations #
|
|
115
157
|
# --------------------------------------------------------------------------- #
|
|
@@ -142,6 +184,13 @@ _DEV_PRONUNCIATIONS: dict[str, str] = {
|
|
|
142
184
|
"postgresql": "postgres",
|
|
143
185
|
"kubectl": "kube control",
|
|
144
186
|
"stackone": "stack one", # org name espeak garbles as "stac kone"
|
|
187
|
+
# "dedupe" glued reads as "de-dup" (short u); split + long "dee" gives "dee doop".
|
|
188
|
+
# Inflections listed explicitly — apply_pronunciations matches whole words only.
|
|
189
|
+
"dedupe": "dee dupe",
|
|
190
|
+
"deduped": "dee duped",
|
|
191
|
+
"deduping": "dee duping",
|
|
192
|
+
"dedupes": "dee dupes",
|
|
193
|
+
"dedup": "dee dupe",
|
|
145
194
|
}
|
|
146
195
|
|
|
147
196
|
|
|
@@ -189,11 +238,24 @@ def _strip_md_inline(line: str) -> str:
|
|
|
189
238
|
return line
|
|
190
239
|
|
|
191
240
|
|
|
192
|
-
def markdown_to_paragraphs(
|
|
241
|
+
def markdown_to_paragraphs(
|
|
242
|
+
text: str,
|
|
243
|
+
*,
|
|
244
|
+
tables: str = "drop",
|
|
245
|
+
strip_emoji_flag: bool = False,
|
|
246
|
+
code_blocks: str = "drop",
|
|
247
|
+
code_placeholder: str = "",
|
|
248
|
+
) -> list[str]:
|
|
193
249
|
"""Reduce Markdown to a list of speakable paragraphs. Headings and list
|
|
194
250
|
items become their own paragraphs (so each gets its own pause). Tables are
|
|
195
|
-
dropped or rendered comma-separated per ``tables``.
|
|
196
|
-
|
|
251
|
+
dropped or rendered comma-separated per ``tables``. Fenced code blocks are
|
|
252
|
+
dropped, or (``code_blocks="placeholder"``) replaced with a spoken
|
|
253
|
+
``code_placeholder`` so a silently-skipped block doesn't sound disjointed."""
|
|
254
|
+
speak_code = code_blocks == "placeholder" and bool(code_placeholder)
|
|
255
|
+
# A function replacement keeps the placeholder literal (no backref parsing);
|
|
256
|
+
# blank lines around it make it a standalone paragraph.
|
|
257
|
+
fenced = f"\n\n{code_placeholder}\n\n" if speak_code else "\n"
|
|
258
|
+
text = re.sub(r"(?ms)^[ \t]*(```|~~~).*?^[ \t]*\1[ \t]*$", lambda _: fenced, text) # fenced code
|
|
197
259
|
text = re.sub(r"```+|~~~+", " ", text) # stray fences
|
|
198
260
|
if strip_emoji_flag:
|
|
199
261
|
text = strip_emoji(text)
|
|
@@ -278,6 +340,14 @@ def markdown_to_paragraphs(text: str, *, tables: str = "drop", strip_emoji_flag:
|
|
|
278
340
|
flush()
|
|
279
341
|
|
|
280
342
|
flush()
|
|
343
|
+
|
|
344
|
+
if speak_code: # collapse runs of adjacent code blocks into one placeholder
|
|
345
|
+
collapsed: list[str] = []
|
|
346
|
+
for para in paragraphs:
|
|
347
|
+
if para == code_placeholder and collapsed and collapsed[-1] == code_placeholder:
|
|
348
|
+
continue
|
|
349
|
+
collapsed.append(para)
|
|
350
|
+
return collapsed
|
|
281
351
|
return paragraphs
|
|
282
352
|
|
|
283
353
|
|
|
@@ -290,11 +360,17 @@ def _shape_paragraph(
|
|
|
290
360
|
text: str,
|
|
291
361
|
*,
|
|
292
362
|
pronunciations: dict[str, str] | None,
|
|
363
|
+
dev_terms_flag: bool,
|
|
293
364
|
expand_units_flag: bool,
|
|
294
365
|
expand_numbers_flag: bool,
|
|
295
366
|
pauses_flag: bool,
|
|
296
367
|
locale: str,
|
|
297
368
|
) -> str:
|
|
369
|
+
# File refs first: it consumes the ":line" digits (so the number stages see a
|
|
370
|
+
# plain "line 42", not a decimal) and forms the spoken basename before the
|
|
371
|
+
# dev-term dict runs over it.
|
|
372
|
+
if dev_terms_flag:
|
|
373
|
+
text = speak_file_refs(text)
|
|
298
374
|
if expand_numbers_flag:
|
|
299
375
|
text = strip_thousands_separators(text)
|
|
300
376
|
if pauses_flag:
|
|
@@ -319,6 +395,8 @@ def normalize_for_speech(
|
|
|
319
395
|
expand_numbers: bool = True,
|
|
320
396
|
pauses: bool = True,
|
|
321
397
|
tables: str = "drop",
|
|
398
|
+
code_blocks: str = "drop",
|
|
399
|
+
code_placeholder: str = "Code block.",
|
|
322
400
|
strip_emoji: bool = False,
|
|
323
401
|
terminal_stops: bool = True,
|
|
324
402
|
locale: str = DEFAULT_LOCALE,
|
|
@@ -333,7 +411,13 @@ def normalize_for_speech(
|
|
|
333
411
|
effective_pronunciations[written.lower()] = spoken
|
|
334
412
|
|
|
335
413
|
if markdown:
|
|
336
|
-
paragraphs = markdown_to_paragraphs(
|
|
414
|
+
paragraphs = markdown_to_paragraphs(
|
|
415
|
+
text,
|
|
416
|
+
tables=tables,
|
|
417
|
+
strip_emoji_flag=strip_emoji,
|
|
418
|
+
code_blocks=code_blocks,
|
|
419
|
+
code_placeholder=code_placeholder,
|
|
420
|
+
)
|
|
337
421
|
else:
|
|
338
422
|
# `strip_emoji` (the bool kwarg) shadows the module function here, so
|
|
339
423
|
# reach for the underlying pattern directly.
|
|
@@ -345,6 +429,7 @@ def normalize_for_speech(
|
|
|
345
429
|
shaped = _shape_paragraph(
|
|
346
430
|
para,
|
|
347
431
|
pronunciations=effective_pronunciations,
|
|
432
|
+
dev_terms_flag=dev_terms,
|
|
348
433
|
expand_units_flag=expand_units_flag,
|
|
349
434
|
expand_numbers_flag=expand_numbers_flag,
|
|
350
435
|
pauses_flag=pauses,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackvox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
|
@@ -133,6 +133,8 @@ def _norm_ns(text=None, file=None, **overrides):
|
|
|
133
133
|
expand_numbers=True,
|
|
134
134
|
pauses=True,
|
|
135
135
|
tables="drop",
|
|
136
|
+
code_blocks="drop",
|
|
137
|
+
code_placeholder="Code block.",
|
|
136
138
|
strip_emoji=False,
|
|
137
139
|
terminal_stops=True,
|
|
138
140
|
locale="en-GB",
|
|
@@ -524,6 +526,26 @@ class TestNormalizeParsing:
|
|
|
524
526
|
assert cli.main() == 0
|
|
525
527
|
assert paths_handler.called
|
|
526
528
|
|
|
529
|
+
def test_code_block_flags_parse(self, mocker):
|
|
530
|
+
norm = mocker.patch.object(cli, "_cmd_normalize", return_value=0)
|
|
531
|
+
mocker.patch.object(
|
|
532
|
+
cli.sys,
|
|
533
|
+
"argv",
|
|
534
|
+
[
|
|
535
|
+
"stackvox",
|
|
536
|
+
"normalize",
|
|
537
|
+
"--code-blocks",
|
|
538
|
+
"placeholder",
|
|
539
|
+
"--code-placeholder",
|
|
540
|
+
"see the chat",
|
|
541
|
+
"hi",
|
|
542
|
+
],
|
|
543
|
+
)
|
|
544
|
+
assert cli.main() == 0
|
|
545
|
+
args = norm.call_args.args[0]
|
|
546
|
+
assert args.code_blocks == "placeholder"
|
|
547
|
+
assert args.code_placeholder == "see the chat"
|
|
548
|
+
|
|
527
549
|
|
|
528
550
|
class TestCmdCancel:
|
|
529
551
|
def test_running_calls_daemon_cancel(self, mocker):
|
|
@@ -8,6 +8,7 @@ from stackvox.text import (
|
|
|
8
8
|
markdown_to_paragraphs,
|
|
9
9
|
normalize_for_speech,
|
|
10
10
|
shape_pauses,
|
|
11
|
+
speak_file_refs,
|
|
11
12
|
strip_emoji,
|
|
12
13
|
strip_thousands_separators,
|
|
13
14
|
versions_to_words,
|
|
@@ -54,6 +55,44 @@ def test_semver_normalizes_end_to_end():
|
|
|
54
55
|
assert "0 point 7.0" not in out
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
# --- file & line references ------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_file_ref_leads_with_line_then_file():
|
|
62
|
+
assert speak_file_refs("unifiedAPIv2.service.ts:666") == "line 666 of unifiedAPIv2 service ts"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_file_ref_drops_directories_to_basename():
|
|
66
|
+
assert speak_file_refs("Open /abs/path/to/module.py:7.") == "Open line 7 of module py."
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_file_ref_line_range():
|
|
70
|
+
assert speak_file_refs("cli.py:100-118") == "lines 100 to 118 of cli py"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_file_ref_line_and_column():
|
|
74
|
+
assert speak_file_refs("foo.ts:666:10") == "line 666, column 10 of foo ts"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_file_ref_leaves_times_ratios_verses_untouched():
|
|
78
|
+
text = "Meet at 12:30, the ratio was 3:1, see John 3:16."
|
|
79
|
+
assert speak_file_refs(text) == text
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_file_ref_leaves_dotted_versions_untouched():
|
|
83
|
+
assert speak_file_refs("bump to 1.2.3 and 0.7.0") == "bump to 1.2.3 and 0.7.0"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_file_ref_normalizes_end_to_end():
|
|
87
|
+
out = normalize_for_speech("See `engine.py:42` for the fix.", markdown=True)
|
|
88
|
+
assert out == "See line 42 of engine py for the fix."
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_file_ref_disabled_with_dev_terms():
|
|
92
|
+
out = normalize_for_speech("See engine.py:42.", markdown=False, dev_terms=False)
|
|
93
|
+
assert out == "See engine.py:42."
|
|
94
|
+
|
|
95
|
+
|
|
57
96
|
# --- units -----------------------------------------------------------------
|
|
58
97
|
|
|
59
98
|
|
|
@@ -74,6 +113,20 @@ def test_math_symbols():
|
|
|
74
113
|
assert expand_units("770 ÷ 2 × 3 = 5").strip() == "770 divided by 2 times 3 equals 5"
|
|
75
114
|
|
|
76
115
|
|
|
116
|
+
def test_tilde_before_number_is_approximately():
|
|
117
|
+
assert expand_units("~123") == "about 123"
|
|
118
|
+
assert expand_units("~ 50 items") == "about 50 items"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_tilde_not_before_number_is_left_alone():
|
|
122
|
+
# home dir / approx-equal — not an approximated quantity
|
|
123
|
+
assert expand_units("~/projects") == "~/projects"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_tilde_approx_end_to_end_with_decimal():
|
|
127
|
+
assert normalize_for_speech("took ~1.5 hours", markdown=False) == "took about 1 point 5 hours."
|
|
128
|
+
|
|
129
|
+
|
|
77
130
|
def test_unknown_locale_falls_back_to_en_gb():
|
|
78
131
|
assert expand_units("£5", locale="xx-YY") == "5 pounds"
|
|
79
132
|
|
|
@@ -136,6 +189,29 @@ def test_fenced_code_dropped():
|
|
|
136
189
|
assert markdown_to_paragraphs(md) == ["Before.", "After."]
|
|
137
190
|
|
|
138
191
|
|
|
192
|
+
def test_fenced_code_replaced_with_placeholder():
|
|
193
|
+
md = "Before.\n\n```py\nprint('hi')\n```\n\nAfter."
|
|
194
|
+
actual = markdown_to_paragraphs(md, code_blocks="placeholder", code_placeholder="see the code")
|
|
195
|
+
assert actual == ["Before.", "see the code", "After."]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_consecutive_code_blocks_collapse_to_one_placeholder():
|
|
199
|
+
md = "```py\na\n```\n\n```py\nb\n```"
|
|
200
|
+
actual = markdown_to_paragraphs(md, code_blocks="placeholder", code_placeholder="see the code")
|
|
201
|
+
assert actual == ["see the code"]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def test_code_placeholder_spoken_end_to_end():
|
|
205
|
+
md = "Here is how:\n\n```py\nx = 1\n```\n\nDone."
|
|
206
|
+
out = normalize_for_speech(md, code_blocks="placeholder", code_placeholder="see the chat history")
|
|
207
|
+
assert out == "Here is how:\nsee the chat history.\nDone."
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def test_code_blocks_dropped_by_default_end_to_end():
|
|
211
|
+
md = "Here is how:\n\n```py\nx = 1\n```\n\nDone."
|
|
212
|
+
assert normalize_for_speech(md) == "Here is how:\nDone."
|
|
213
|
+
|
|
214
|
+
|
|
139
215
|
def test_tables_dropped_by_default():
|
|
140
216
|
md = "| A | B |\n| --- | --- |\n| 1 | 2 |"
|
|
141
217
|
assert markdown_to_paragraphs(md) == []
|
|
@@ -244,6 +320,18 @@ def test_dev_terms_leave_correctly_voiced_acronyms_alone():
|
|
|
244
320
|
assert "API" in out
|
|
245
321
|
|
|
246
322
|
|
|
323
|
+
def test_dedupe_respelled_for_speech():
|
|
324
|
+
assert normalize_for_speech("dedupe the list", markdown=False) == "dee dupe the list."
|
|
325
|
+
# inflections and case
|
|
326
|
+
assert "dee duped" in normalize_for_speech("we Deduped it", markdown=False)
|
|
327
|
+
assert "dee duping" in normalize_for_speech("deduping now", markdown=False)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_dedupe_does_not_touch_other_words():
|
|
331
|
+
# whole-word only — must not rewrite the middle of a longer token
|
|
332
|
+
assert "dee dupe" not in normalize_for_speech("dedupelicated", markdown=False)
|
|
333
|
+
|
|
334
|
+
|
|
247
335
|
def test_dev_terms_can_be_disabled():
|
|
248
336
|
assert normalize_for_speech("Use the CLI.", markdown=False, dev_terms=False) == "Use the CLI."
|
|
249
337
|
|
|
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
|
|
File without changes
|