Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/speech-normalization.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ the mechanism now and let each caller keep evolving its own dictionary.
- Backward compatible — existing `speak`/`synthesize` behaviour unchanged unless opted in.

**Non-goals**
- Baking domain pronunciations (agy, StackOne, Redis…) into StackVox.
- Baking **domain/org** pronunciations (agy, StackOne, Redis…) into StackVox — those stay per-consumer. Generic fixes for dev acronyms espeak plain *mispronounces* (CLI→"kligh", AWS→"awz", URI→"yuri", …) **are** shipped in core, on by default via `dev_terms` / `_DEV_PRONUNCIATIONS`.
- Owning the blog's `say:` authoring directive (a Markdown-authoring convention; Claude responses never contain it).
- Changing default synthesis behaviour or the daemon protocol.

Expand Down Expand Up @@ -94,7 +94,7 @@ stackvox normalize --file resp.md # print normalized te
### Illustrative dictionaries (stay per-consumer)

- **Blog:** `agy, 1M, 175K, xhigh, SessionStart, PermissionRequest, StackOne, OAuth, Behan, Redis`
- **speaklast (to tune by listening):** `StackOne, Redis, OAuth, CLI, npm, async, repo, MCP, …` — Claude leans on tool/lib/identifier names, so this dict will grow differently.
- **speaklast (to tune by listening):** `StackOne, Redis, repo, …` — domain/product names Claude leans on. Generic dev acronyms (CLI, CI, AWS, URI, IAM…) now live in core's default `dev_terms` map, so they don't need repeating per-consumer.

## 6. Backward compatibility & rollout

Expand Down
7 changes: 7 additions & 0 deletions stackvox/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ def _add_normalize_args(parser: argparse.ArgumentParser, *, with_switch: bool) -
type=Path,
help="JSON file mapping written -> spoken forms (whole-word, case-insensitive)",
)
parser.add_argument(
"--no-dev-terms",
dest="dev_terms",
action="store_false",
help="Do not spell out dev acronyms espeak mispronounces (CLI, CI, IDE, AWS, URI, IAM, ...)",
)
parser.add_argument(
"--no-expand-units",
dest="expand_units",
Expand Down Expand Up @@ -268,6 +274,7 @@ def _normalize_kwargs(args: argparse.Namespace) -> dict:
return {
"markdown": args.markdown,
"pronunciations": _load_pronunciations(args.pronunciations),
"dev_terms": args.dev_terms,
"expand_units": args.expand_units,
"expand_numbers": args.expand_numbers,
"pauses": args.pauses,
Expand Down
30 changes: 29 additions & 1 deletion stackvox/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,28 @@ def apply_pronunciations(text: str, mapping: dict[str, str] | None) -> str:
return text


# Dev acronyms/terms espeak mispronounces — it reads them as a word ("CLI" ->
# "kligh", "AWS" -> "awz", "URI" -> "yuri") instead of spelling them out.
# Applied by default (dev_terms=True), whole-word and case-insensitive, so
# lowercase "cli" is fixed too. Compound keys (ci/cd) precede their parts (ci)
# so the specific form wins. Only terms espeak gets WRONG are here — API, URL,
# JSON, YAML, HTTP, CRUD, nginx, etc. already voice correctly and are left alone.
_DEV_PRONUNCIATIONS: dict[str, str] = {
"ci/cd": "C I C D",
"cli": "C L I",
"ci": "C I",
"ide": "I D E",
"aws": "A W S",
"uri": "U R I",
"iam": "I A M",
"saas": "sass",
"paas": "pass",
"tui": "T U I",
"postgresql": "postgres",
"kubectl": "kube control",
}


# --------------------------------------------------------------------------- #
# Pauses #
# --------------------------------------------------------------------------- #
Expand Down Expand Up @@ -274,6 +296,7 @@ def normalize_for_speech(
*,
markdown: bool = True,
pronunciations: dict[str, str] | None = None,
dev_terms: bool = True,
expand_units: bool = True,
expand_numbers: bool = True,
pauses: bool = True,
Expand All @@ -285,6 +308,11 @@ def normalize_for_speech(
"""Normalize ``text`` into speakable prose. Returns paragraphs joined by
newlines. See ``docs/speech-normalization.md`` for the full contract."""
expand_units_flag, expand_numbers_flag = expand_units, expand_numbers
# Built-in dev-term fixes first; caller-supplied pronunciations override them
# (keyed case-insensitively, so a caller's "CLI" beats the default "cli").
effective_pronunciations: dict[str, str] = dict(_DEV_PRONUNCIATIONS) if dev_terms else {}
for written, spoken in (pronunciations or {}).items():
effective_pronunciations[written.lower()] = spoken

if markdown:
paragraphs = markdown_to_paragraphs(text, tables=tables, strip_emoji_flag=strip_emoji)
Expand All @@ -298,7 +326,7 @@ def normalize_for_speech(
for para in paragraphs:
shaped = _shape_paragraph(
para,
pronunciations=pronunciations,
pronunciations=effective_pronunciations,
expand_units_flag=expand_units_flag,
expand_numbers_flag=expand_numbers_flag,
pauses_flag=pauses,
Expand Down
1 change: 1 addition & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def _norm_ns(text=None, file=None, **overrides):
file=file,
markdown=True,
pronunciations=None,
dev_terms=True,
expand_units=True,
expand_numbers=True,
pauses=True,
Expand Down
25 changes: 25 additions & 0 deletions tests/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,3 +195,28 @@ def test_speaklast_style_normalization():
def test_plain_text_mode_splits_paragraphs():
out = normalize_for_speech("First para.\n\nSecond para.", markdown=False)
assert out == "First para.\nSecond para."


def test_dev_acronyms_are_spelled_out():
assert normalize_for_speech("Use the CLI.", markdown=False) == "Use the C L I."
assert "C L I" in normalize_for_speech("the cli tool", markdown=False) # lowercase too
assert "A W S" in normalize_for_speech("deploy to AWS", markdown=False)
assert "U R I" in normalize_for_speech("parse the URI", markdown=False)
assert "C I C D" in normalize_for_speech("the CI/CD pipeline", markdown=False)


def test_dev_terms_leave_correctly_voiced_acronyms_alone():
out = normalize_for_speech("Send JSON over HTTP to the API.", markdown=False)
assert "JSON" in out
assert "HTTP" in out
assert "API" in out


def test_dev_terms_can_be_disabled():
assert normalize_for_speech("Use the CLI.", markdown=False, dev_terms=False) == "Use the CLI."


def test_caller_pronunciations_override_dev_terms():
out = normalize_for_speech("Use the CLI.", markdown=False, pronunciations={"CLI": "command line"})
assert "command line" in out
assert "C L I" not in out
Loading