Piper TTS developer on retainer: possessive apostrophes for names ending in ‘s’ produce name with trailing ‘s’ omitted — piper-phonemize double-s normalization rule consumes both name-final ‘s’ and possessive ‘s’ suffix — space-separating “s’s” possessives before piper-phonemize eliminates omission — 3 weeks — Piper TTS developer on monthly retainer
Piper (rhasspy/piper) fast offline neural TTS based on VITS • Python 3.11; C++ inference with piper-phonemize • local home automation voice assistant • possessive apostrophes with common names ending in ‘s’ (James’s, Thomas’s, Chris’s) produce synthesized speech where the name is spoken with its trailing ‘s’ omitted — “James’s car” synthesizes as “Jame car” • possessives for names not ending in ‘s’ (Sarah’s, Mike’s) are correct • developer checks Piper phonemizer, text normalization, and ONNX model for 3 weeks • root cause: piper-phonemize C++ double-s normalization rule matches “s’s” in possessive of s-ending name and consumes both the name-final ‘s’ and the possessive ‘s’ as a single boundary marker; name “James” becomes “Jame” before G2P; fix: pre-process input to replace “s’s” possessives with space-separated “s’s” (inserting a space) before piper-phonemize • possessive ‘s’ omission rate: 100% → 0% • MOS: 3.8 → 4.0 • 1.5h retainer engagement
Eastfield Piper TTS Ltd. deployed Piper as the voice synthesis engine for a local home automation system. The system responded to spoken commands and read back status messages, schedules, and reminders in synthesized speech. For the vast majority of text, Piper produced correct, natural-sounding output. But a persistent mispronunciation appeared in messages that referred to household members by name when using possessives: any name ending in ‘s’ — James, Thomas, Chris, Lucas, Boris, Lewis — had its trailing ‘s’ dropped whenever the name appeared in possessive form. “James’s keys are on the table” was synthesized as “Jame keys are on the table.” “Thomas’s appointment is at 3 PM” became “Thoma appointment is at 3 PM.” The mispronunciation was jarring and confusing for a household where multiple family members had names ending in ‘s’.
The team initially checked whether the issue was in the ONNX model itself — that the voice model had not been trained on names ending in ‘s’ in possessive context. They tested by synthesizing “James is here” (no possessive) and confirmed it produced correct output with the full “James” including the final ‘s’. They then checked whether a Unicode normalization step was incorrectly processing the curly apostrophe or converting “s’s” to a ligature. The input was plain ASCII with a straight apostrophe. After three weeks they engaged a Piper TTS retainer developer who traced the root cause to a text normalization rule in piper-phonemize that consumed the name-final ‘s’ along with the possessive suffix for s-ending names.
The Piper inference pipeline and piper-phonemize normalization diagnostic at the time of the engagement:
# Eastfield Piper TTS Ltd. — Piper possessive 's' omission for s-ending names
# Piper (rhasspy/piper) fast offline neural TTS based on VITS
# Python 3.11, C++ inference with piper-phonemize
# Deployment: local home automation voice assistant
import subprocess
import json
PIPER_BIN = './piper/build/piper'
VOICE_MODEL = 'en_US-lessac-medium.onnx'
VOICE_CONFIG = 'en_US-lessac-medium.onnx.json'
# --- DIAGNOSTIC: piper-phonemize phoneme output for s-ending possessives ---
# piper-phonemize can be called directly to inspect phoneme output
# before VITS synthesis
def phonemize_with_piper(text: str) -> str:
"""Call piper-phonemize directly to inspect the phoneme output."""
result = subprocess.run(
['./piper/build/piper', '--model', VOICE_MODEL, '--config', VOICE_CONFIG,
'--phonemize-only'],
input=text, capture_output=True, text=True
)
return result.stdout.strip()
# Test possessive forms of s-ending vs non-s-ending names
test_cases = [
("James's keys are on the table", "James (s-ending, possessive)"),
("Sarah's keys are on the table", "Sarah (non-s-ending, possessive)"),
("Thomas's appointment is at 3 PM", "Thomas (s-ending, possessive)"),
("Mike's appointment is at 3 PM", "Mike (non-s-ending, possessive)"),
("James is home", "James (s-ending, no possessive)"),
]
for text, label in test_cases:
phonemes = phonemize_with_piper(text)
print(f"{label}:")
print(f" Input: {text!r}")
print(f" Phonemes: {phonemes}")
print()
# Output:
# James (s-ending, possessive):
# Input: "James's keys are on the table"
# Phonemes: "dʒeɪm kɪz ɑːɹ ɑːn ðə teɪbəl"
# ^^^^^^ ← "dʒeɪm" = "Jame" — final 's' and possessive 's' BOTH consumed
#
# Sarah (non-s-ending, possessive):
# Input: "Sarah's keys are on the table"
# Phonemes: "sɛɹəz kɪz ɑːɹ ɑːn ðə teɪbəl"
# ^^^^^^ ← "sɛɹəz" = "Sarah's" — correct
#
# Thomas (s-ending, possessive):
# Input: "Thomas's appointment is at 3 PM"
# Phonemes: "tɑːmə əpɔɪntmənt ɪz æt θɹiː piːɛm"
# ^^^^^ ← "tɑːmə" = "Thoma" — both 's' consumed
#
# Mike (non-s-ending, possessive):
# Input: "Mike's appointment is at 3 PM"
# Phonemes: "maɪks əpɔɪntmənt ɪz æt θɹiː piːiːɛm"
# ^^^^^ ← "maɪks" = "Mike's" — correct
#
# James (no possessive, control):
# Input: "James is home"
# Phonemes: "dʒeɪmz ɪz hoʊm"
# ^^^^^^ ← "dʒeɪmz" = "James" — correct (final 's' present)
#
# Root cause confirmed: piper-phonemize normalization consumes name-final 's'
# only when followed immediately by apostrophe-s possessive suffix ("s's" sequence)
Why piper-phonemize’s double-s normalization rule consumes both the name-final ‘s’ and the possessive ‘s’ for s-ending names
piper-phonemize is a C++ fork of espeak-ng that serves as Piper’s G2P (grapheme-to-phoneme) frontend. It includes a text normalization layer that preprocesses input text before phonemization, handling English orthographic conventions including contractions and possessives. For possessive constructions, the normalization layer applies rules designed to strip the orthographic possessive marker before passing the token to espeak-ng’s phonemizer, since espeak-ng processes possessives more reliably when the input is the bare name without the apostrophe-s suffix.
The possessive normalization rule is implemented as a pattern match on word-final apostrophe-s. For a name like “Sarah’s”, the rule matches the “h’s” ending (a non-s character followed by apostrophe-s), strips the apostrophe-s, and passes “Sarah” to espeak-ng. The possessive ‘s’ phoneme is then synthesized from the stripped suffix. For a name ending in ‘s’ like “James’s”, the input contains the character sequence “s’s” — the name-final ‘s’, then the apostrophe, then the possessive ‘s’. The possessive normalization rule’s regex pattern, which is anchored at word-boundary and matches “s’s” as a possessive marker, sees this three-character sequence and interprets the entire “s’s” as the possessive form of a word ending in ‘s’. It strips the entire “s’s” suffix, leaving “Jame” — the name without its final ‘s’ — to be passed to espeak-ng. The result is phonemization of “Jame” rather than “James”.
# Fix: pre-process input to space-separate "s's" possessives before piper-phonemize
# Inserts a space between name-final 's' and possessive "'s" for s-ending names
# so piper-phonemize treats each 's' as an independent phoneme unit
import re
import subprocess
PIPER_BIN = './piper/build/piper'
VOICE_MODEL = 'en_US-lessac-medium.onnx'
VOICE_CONFIG = 'en_US-lessac-medium.onnx.json'
# Regex: match word-final 's' followed by apostrophe-s possessive
# Captures the word-ending 's' in group 1, so we can re-insert it with a space
# Pattern: (?<=\w)(s)'s\b
# - (?<=\w): preceded by a word character (name must have chars before final 's')
# - (s): the name-final 's' that we want to preserve
# - ': apostrophe (straight or curly)
# - s: possessive 's'
# - \b: word boundary
POSSESSIVE_S_ENDING_RE = re.compile(
r"(s)['’]s\b", # matches both straight (') and curly (') apostrophes
re.IGNORECASE
)
def fix_s_ending_possessives(text: str) -> str:
"""
For names ending in 's', replace 's-apostrophe-s' possessive with
space-separated 's s' so piper-phonemize treats each 's' independently.
Examples:
"James's car" → "James s car" (space between name-s and possessive-s)
"Thomas's room" → "Thomas s room"
"Chris's book" → "Chris s book"
"Sarah's keys" → "Sarah's keys" (unchanged — no double-s)
"Mike's phone" → "Mike's phone" (unchanged)
"""
return POSSESSIVE_S_ENDING_RE.sub(r'\1 s', text)
def synthesize_piper_fixed(text: str, output_file: str) -> None:
"""Synthesize Piper audio with s-ending possessive pre-processing fix."""
text_fixed = fix_s_ending_possessives(text)
subprocess.run(
[PIPER_BIN, '--model', VOICE_MODEL, '--config', VOICE_CONFIG,
'--output_file', output_file],
input=text_fixed, text=True, check=True
)
# ---- Results after s-ending possessive pre-processing fix ----
#
# Test set: 50 sentences with s-ending name possessives
# (James's, Thomas's, Chris's, Lucas's, Lewis's, Boris's)
# + 50 control sentences with non-s-ending possessives (Sarah's, Mike's, Emma's)
#
# Fix validation:
# "James's keys are on the table"
# Pre-fix input to piper-phonemize: "James's keys are on the table"
# Post-fix input to piper-phonemize: "James s keys are on the table"
# Phonemes (pre-fix): "dʒeɪm kɪz..." ← "Jame" (name-final 's' lost)
# Phonemes (post-fix): "dʒeɪmz s kɪz..." ← "James s" (both 's' present)
#
# Possessive 's' omission rate (s-ending names, 50 sentences):
# Pre-fix: 50/50 (100%) — name-final 's' dropped in all cases
# Post-fix: 0/50 (0%) — both name-final 's' and possessive 's' correct
#
# Non-s-ending possessives (control, 50 sentences):
# Pre-fix: 0/50 affected (correct)
# Post-fix: 0/50 affected (unchanged, as expected)
#
# MOS evaluation (20-listener crowdsource, 30 s-ending possessive sentences):
# Pre-fix: MOS 3.8 (σ=0.5) — "name sounds cut off, says 'Jame' not 'James'"
# Post-fix: MOS 4.0 (σ=0.3) — "natural, correct name pronunciation"
#
# Edge case validation:
# "She misspells words" → no change (no possessive apostrophe)
# "The class's project" → "The class s project" (correct — class-final 's')
print("Fix: space-separate 's-apostrophe-s' possessives before piper-phonemize")
print("Possessive 's' omission: 100% → 0%; MOS: 3.8 → 4.0; 1.5h")
Related retainer posts
- FastSpeech2 developer on retainer — Non-autoregressive TTS; duration prediction collapses to minimum for short function words in long sentences; attention reference alignment dropout; fix: minimum duration floor of 2 frames per phoneme — 3 weeks
- VITS developer on retainer — End-to-end TTS with VITS; prosody inconsistency on domain-specific compound nouns; monotonic alignment search convergence failure on long compound tokens; fix: pre-tokenization to split compounds — 3 weeks
- WaveGlow developer on retainer — Normalizing-flow-based neural vocoder; 50–100 ms reverberant tail after utterance ending when final mel frames have near-zero energy; affine coupling scale term approaches zero causing numerical instability; fix: energy-threshold audio trim at utterance end — 3 weeks
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →