From 8b7ac40f511a52400e3a7c33271f0624bad595af Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 7 Jun 2026 03:40:39 -0400 Subject: [PATCH] Weak endings carry their coda The weak-end key is now the full final-syllable rime in its own keyspace, and groups advertise their founding rime's final syllable: infancy still joins see/be/me (open IY), but screams (IY M Z) no longer rides the divinity/entirety family. Co-Authored-By: Claude Opus 4.8 (1M context) --- app.py | 21 ++++++++++++++++----- tests/test_rhymes.py | 10 ++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/app.py b/app.py index 0575ccb..6ec1ad6 100644 --- a/app.py +++ b/app.py @@ -211,6 +211,11 @@ def founding_projections(key: str) -> dict[str, str]: mk2 = _m2_key(ph) if mk2: out["multi2"] = mk2 + # the founding rime's final syllable, for weak-ending joins + for i in range(len(ph) - 1, -1, -1): + if ph[i] in ARPA_VOWELS: + out["weak"] = "w:" + " ".join(ph[i:]) + break elif key.startswith("v:"): # vowel tail out["slant"] = key mk = _multi_key(key[2:].split()) @@ -222,6 +227,8 @@ def founding_projections(key: str) -> dict[str, str]: out["multi2"] = key elif key.startswith("c:"): out["vc"] = key + elif key.startswith("w:"): + out["weak"] = key return out @@ -344,13 +351,17 @@ def multi_keys(word: str) -> tuple[str, ...]: def weak_end_key(word: str) -> str | None: - """The bare final vowel, stress be damned — at line ends poets rhyme - the weak syllable (infancy / see, eternity / be).""" + """Final-syllable rime, stress be damned — at line ends poets rhyme + the weak syllable (infancy / see), but the coda still has to agree + (divinity does not rhyme screams).""" ph = phones_for(word) if not ph: return None - vowels = _all_vowels(ph) - return ("v:" + vowels[-1]) if vowels else None + pl = ph.split() + for i in range(len(pl) - 1, -1, -1): + if pl[i][-1].isdigit(): + return "w:" + DIGITS.sub("", " ".join(pl[i:])) + return None def slant_key(word: str) -> str | None: @@ -627,7 +638,7 @@ def analyze(draft: Draft): # A leftover ending first tries to JOIN an existing group whose # founding sound shares its vowel tail (time -> the mind/find group); # otherwise leftovers form a new slant group among themselves. - group_by_slant = gmap_for("slant") + group_by_slant = {**gmap_for("slant"), **gmap_for("weak")} by_slant = defaultdict(list) for t in tokens: if t["is_end"] and id(t) not in grouped: diff --git a/tests/test_rhymes.py b/tests/test_rhymes.py index c9b0a6d..2ca2ff8 100644 --- a/tests/test_rhymes.py +++ b/tests/test_rhymes.py @@ -435,3 +435,13 @@ def test_weak_ending_rhymes_at_line_end(): def test_cmu_override_stasis(): # CMU transcribes stasis as "STAH-seez"; everyone says STAY-sis group_with("Oasis of stasis", "oasis", "stasis") + + +def test_weak_ending_requires_matching_coda(): + # divinity ends open (..tee); screams ends IY M Z — same vowel, + # different coda, not the same family + text = ("Roots surrounding our entirety\n" + "Tangled in divinity\n" + "Weaving heaven among hellish screams") + fam = group_with(text, "entirety", "divinity") + assert "screams" not in fam