Found while testing crates/tts-text/src/normalize.rs:352 (parse_amount, used by parse_money at :266)
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
#954 let fraction operands run to any cardinal length, so the "7/2026"
left after an impossible date's "13/" read "seven over two thousand
twenty-six" instead of a slash and the year (normalize::tests::dates).
Three digits covers every case #954 added (7/100, 100/7).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Narrations take schema v14 (main's v13 is audio streams): v13_to_v14, docs updated.
- neural-voices is off by default in the desktop app, like whisper; the model download stays
an explicit, confirmed job.
- Removed the debug probe2 test module from tts-text.
- project_subset prunes narrations of items it drops.
- unique_wav_path is bounded (99 999 names) and returns an error instead of looping.
- narration.rs uses web_time::Instant (wasm clock test); tts.* commands get the
"Text to Speech" shortcut category, with es/ja rows for it and Edit Narration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fix round from Claude's P2b review. The normalizer's terminator run now picks
the strongest terminator (? over ! over .) and carries it through a new
backwards-compatible Token::SentenceEndWith(char) (SentenceEnd still means
"."); phonemize attaches ? and ! to the sentence's last word so Kokoro can
shape intonation: "Ready?" -> "ɹˈɛdɪ?", "Go!" -> "ɡˈO!", "Really?!" -> ?.
Also converts the remaining direct indexes in non-test code to
get()/get_mut()/iterators (letter_to_sound slices, the tion/sion and
first-vowel stress writes, the scanner's char access).
New cases: nine terminator tests in normalize (incl. "Is it 1999?",
"Call 555-0134!", "Wait... what?", "Are you there?!") and nine phonemize
assertions (Ready?, Go!, Done., No!!, Are you there?!, ...). All
SentenceEnd-matching tests updated.
Gates: fmt, clippy -p filmcraft-tts-text --all-targets -D warnings, tests
(37 suites), xtask layers, xtask wasm - all green.
🤖 Written by Merlin (Hermes Agent, profile merlin)
New module phonemize in filmcraft-tts-text: normalize(script) -> words ->
Kokoro phoneme symbols, the interface Claude's Kokoro code (P3) calls. Public
API per the brief's contract: Lexicon (CMUdict parser, 16 MiB / 500 000-entry
caps, malformed lines skipped and counted), Overrides (case-insensitive,
raw /phonemes/ or respellings, capped at 1 000 entries of 200 chars),
Chunk::Phonemes/Pause with MAX_CHUNK_CHARS = 400, phonemize(), and SYMBOLS —
the exact character set emitted (ARPAbet→Kokoro table from the brief, stress
marks ˈ/ˌ before the vowel, function words unstressed, punctuation attached).
Lookup order: overrides → dictionary (first pronunciation) → morphology
(-s/-'s with voicing, -ed, -ing, -er, -ly, un-, re-) → letter-to-sound rules
written from scratch (tion/sion, ture/sure, ough, igh, digraphs, silent e,
soft c/g, doubled consonants, final le). Hyphenated words split per part.
Tests: CMUdict parse table (alternates, comments, malformed counting, caps),
46 exact phonemize cases, 36 letter-to-sound cases, chunk splitting at
commas ≤ 400 chars, a symbol-set test over all normalizer inputs plus
hostile ones, and catch_unwind hostiles (huge, empty, emoji, RTL, NUL,
10 000-letter word, 1 001 overrides). Optional measured-accuracy test
(FILMCRAFT_CMUDICT): held-out CMUdict words → 9.1% word accuracy, 37.9%
phoneme error rate (229 words, 2026-10-08). CMUdict is downloaded with the
voices at run time, never committed.
Gates: fmt, clippy -p filmcraft-tts-text --all-targets -D warnings, tests
(37 suites), xtask layers, xtask wasm (44 crates) — all green.
🤖 Written by Merlin (Hermes Agent, profile merlin)
New L2 crate filmcraft-tts-text: normalize(script, lang) -> Vec<Token> for the
Text to Speech text front end (P2a). English only (EnUs/EnGb), per the amended
brief. Handles cardinals to 999 999 999 999, negatives, decimals, ordinals,
money ($, €, £), percent, years (1100-2099), clock times, numeric and
abbreviated dates, phone-like digit groups, Mr./Mrs./Dr./St./Mt./vs./etc./e.g./
i.e./a.m./p.m., spelled acronyms, &/+@#/, and [pause Ns|ms] markers capped at
10 s. Pure Rust, deps serde + thiserror only, wasm-clean.
Never crashes (AGENTS.md §0): >1 MiB is NormalizeError::TooLong, oversized and
leading-zero numbers are read digit by digit, malformed pause markers become
plain text, all slicing is get()/boundary-safe, and a catch_unwind suite covers
empty/whitespace/oversized/10k-digit/$$$$/mixed-script/emoji/RTL/combining/NUL
inputs plus determinism.
Fix round (Claude review): a '.' after a number now ends the sentence (year
rule and money/decimal follow-through), so "in 1999." reads as a year with a
SentenceEnd and "$1,234,567." gets its final SentenceEnd; new table cases for
sentence-final numbers and the year rule under punctuation (., ,, !, ()); the
remaining direct indexes in non-test code converted to get()/first()/last().
Gates: fmt, clippy -p filmcraft-tts-text --all-targets -D warnings, tests
(25 suites, ~140 table cases), cargo xtask layers, cargo xtask wasm all green.
cargo xtask ci not run this round (Claude runs it when merging); it stops on a
pre-existing filmcraft-engine shortcuts test failure unrelated to this crate.
🤖 Written by Merlin (Hermes Agent, profile merlin)