TTS Formatting Rules — Podcast Lane (Compliance)
TTS Formatting Rules — Podcast Lane (Compliance)
Owner: Eva (podcast lane). Set as card t_dc7f9b0e, 2026-10-21.Authoritative sources (ground truth for every rule below):
/home/eva/.hermes/profiles/eva/scripts/podcast_script_template.py—fmt_ticker_to_name(),fmt_pause(),fmt_currency()/home/eva/.hermes/profiles/eva/scripts/number_to_words.py—num_to_words(),fmt_percent_natural()
If any downstream prompt, cron, or script contradicts this file, this file wins — and the source functions above (not the prose) are what actually run.
Rule 1 — Full company names, never stock codes
Every ticker symbol in a script is replaced with its full company name before TTS renders. Handled by fmt_ticker_to_name().
Ticker-to-company map (as of 2026-10-21, verbatim from the source):
| code | spoken name | code | spoken name | |
|---|---|---|---|---|
| BHP | BHP Billiton | ALL | Aristocrat Leisure | |
| RIO | Rio Tinto | REA | REA Group | |
| FMG | Fortescue Metals | GMG | Goodman Group | |
| CBA | Commonwealth Bank | TCL | Transurban | |
| NAB | National Australia Bank | JHX | James Hardie | |
| WBC | Westpac | ASX | the ASX Group | |
| ANZ | ANZ Bank | NVDA | Nvidia | |
| MQG | Macquarie Group | TSLA | Tesla | |
| CSL | CSL Limited | AAPL | Apple | |
| WES | Wesfarmers | MSFT | Microsoft | |
| WOW | Woolworths | GOOGL | Alphabet | |
| TLS | Telstra | AMZN | Amazon | |
| STO | Santos | META | Meta | |
| BOE | Boss Energy | ZIP | Zip Co | |
| DYL | Deep Yellow | CXO | Core Lithium | |
| PDN | Paladin Energy | SUN | Suncorp | |
| S32 | South 32 | IAG | Insurance Australia Group | |
| BUB | Bubs Australia | QBE | QBE Insurance | |
| MIN | Mineral Resources | PLS | Pilbara Minerals | |
| IGO | IGO Limited | LYC | Lynas Rare Earths | |
| NVX | Novonix | SEMI | the semiconductor ETF | |
| NDQ | the Nasdaq 100 ETF |
Extension rule: whenever a new name enters the watchlist, extend the map in fmt_ticker_to_name() — and the ticker list inside fmt_pause() (they are two separate hard-coded lists and must stay in sync; adding to only one means the name is spoken but with no pause). Same for the movers/all_names/single_names lists in the morning/afternoon/weekly generators when the name should appear in the movers section.
Rule 2 — Natural pauses before company names and percentages
TTS commas create spoken pauses. fmt_pause():
- inserts a comma before any map ticker not already comma-prefixed:
"…and then, BHP, it moved up…" - inserts a comma before
percent/per cent:"…the ASX 200, it fell, one point eight percent…" - collapses doubled commas
The generators pass company-name phrases through fmt_pause() explicitly (e.g. fmt_pause('S&P 500'), fmt_pause('ASX 200'), fmt_pause('Hang Seng')).
Rule 3 — Decimal numbers as words
num_to_words() renders decimals in spoken word form, never digit strings:
| value | spoken (whole part 0) | spoken (with whole part) |
|---|---|---|
| 0.50 | half | "…and a half" |
| 0.25 | a quarter | "…and a quarter" |
| 0.75 | three quarters | "…and three quarters" |
| 0.10 | a tenth | "one point one" |
| 0.20 | two tenths | "one point two" |
| 0.30 | three tenths | "one point three" |
| 0.40 | four tenths | (pattern: N point d) |
| 0.60–0.90 | six/nine tenths | (pattern) |
| other | point one two three (digit-by-digit) | e.g. "two point zero five" |
Generic path: whole number then point then digits spoken one by one, trailing zeros stripped. Negatives are minus + magnitude. Large numbers render as thousand / million naturally.
Rule 4 — Zero is silent: say "flat"
num_to_words(0) returns "" — zero is never read aloud in a magnitude context. Every change-phrase path must therefore substitute a word, never an empty magnitude.
Source of truth (as of 2026-10-21, card t_9f0e8a99):
| helper | file | zero → | non-zero → |
|---|---|---|---|
fmt_percent_natural(n) | number_to_words.py | flat | up N percent / down N percent |
describe_move(chg) | podcast_script_template.py | finished unchanged | gained N percent / lost N percent (spoken words) |
change_phrase(chg) | podcast_script_template.py | flat | up N.N percent / down N.N percent (digits) |
describe_move() is used where the sentence carries no verb of its own (morning show: "The Nasdaq finished unchanged."); change_phrase() where a verb is already supplied (weekly: "the Nasdaq was flat"). Flatness tolerance is 0.05% — a move that rounds to 0.0% is treated as flat.
Hard rule: never emit an empty magnitude. Banned outputs — "The Nasdaq lost, percent." / "down 0.0 percent" / "up percent". fmt_percent_natural() also guards the sub-0.1% case (0.001% previously rendered as "up percent" because num_to_words(0) is silent).
Rule 5 — Symbols are spelled out
Per fmt_currency() and the script header rules (Young, 2026-10-01):
$→X dollars("five dollars", not "$5")%→percent&→and(e.g. "S&P 500" spoken "S and P five hundred")- No markdown symbols in TTS output (headers,
#,*must never reach the voice)
Rule 6 — Conversational delivery
Short sentences. Natural rhythm. Conversational scaffolding allowed: "look", "here's the thing", "what's interesting is…". These per-script rules come from the template header (Young, 2026-10-01) and apply to all three shows (morning, afternoon wrap, weekly deep dive).
Known deviations / follow-ups (found 2026-10-21)
- Afternoon + weekly movers lines emit raw digit percentages (
up {c:.1f} percent, e.g. "up 1.8 percent" instead of "up one point eight percent") — the movers strings ingenerate_afternoon_script()andgenerate_weekly_script()skipnum_to_words()/fmt_percent_natural(). Violates Rule 3 in those sections only. Cathed lines elsewhere are compliant.- Update (card t_9f0e8a99): these movers lines now route through
change_phrase(), which keeps the digit wording but eliminates the zero case ("flat" instead of "down 0.0 percent"). The Rule 3 deviation (digits vs spoken words) is still open — fixing it would change the voice of every afternoon/weekly episode, so it needs its own card.
- Update (card t_9f0e8a99): these movers lines now route through
- Map sync hazard: ticker list exists separately in
fmt_ticker_to_name(),fmt_pause(), and three generators' name arrays — five places to update per new watchlist name (see Rule 1 extension rule). fmt_currency()only matches$<digits>, not$-prefixed words or ranges — scripts using "five dollars" spellings directly are already compliant.
Deviations were observed, not fixed, in this card — code changes belong to a podcast-lane build task, not the compliance snapshot.

