From faa35229563b29e1a7f40191da28dd4bb7a99640 Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 7 Jun 2026 12:15:27 -0400 Subject: [PATCH] Repetition is refrain, not rhyme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Identical words/phrases no longer form a color group on their own — a group colors only once a differing word rhymes into it. Repeated endings still share a scheme letter (refrains scan), and a repeated unrhymed ending no longer flags as "unanswered". Co-Authored-By: Claude Opus 4.8 (1M context) --- app.py | 25 ++++++++++++++++--------- tests/test_rhymes.py | 15 +++++++++++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/app.py b/app.py index cceb1cf..c323f4f 100644 --- a/app.py +++ b/app.py @@ -621,9 +621,9 @@ def analyze(draft: Draft): # distinctness by anchor word, so "fire burns" can't pose as a # rhyme partner for the "fire" it starts with distinct = {t["word"].split()[0].lower() for t in toks} - end_count = sum(t["is_end"] for t in toks) - if len(distinct) < 2 and end_count < 2: - continue # the same word repeated mid-line isn't a rhyme + if len(distinct) < 2: + continue # repetition is refrain, not rhyme — a group only + # colors once a DIFFERING word rhymes into it if all(" " in t["word"] and t["word"].split()[0] in STOPWORDS for t in toks): continue # stopword-anchored phrases need a real-word partner @@ -994,6 +994,10 @@ def analyze(draft: Draft): for t in [*tokens, *phrases]: if t["is_end"] and t["gid"] is not None: end_gid.setdefault(t["line"], t["gid"]) + last_tok = {} + for t in tokens: + if t["is_end"]: + last_tok[t["line"]] = t stanza_lines = defaultdict(list) for i, s in enumerate(sids): if s is not None: @@ -1004,7 +1008,13 @@ def analyze(draft: Draft): letters, order = [], {} for i in stanza_lines[s]: gid = end_gid.get(i) - key = gid if gid is not None else f"solo:{i}" + if gid is not None: + key = gid + elif i in last_tok: + # refrains share a letter even though they don't color + key = "w:" + last_tok[i]["word"].lower() + else: + key = f"solo:{i}" if key not in order: order[key] = len(order) letters.append(order[key]) @@ -1110,12 +1120,9 @@ def analyze(draft: Draft): # unanswered endings: line-ends still waiting for a rhyme partner — # the open loops that tell a writer where to strike next open_out = [] - last_tok = {} - for t in tokens: - if t["is_end"]: - last_tok[t["line"]] = t + end_word_counts = Counter(t["word"].lower() for t in last_tok.values()) for i, t in last_tok.items(): - if i not in end_gid: + if i not in end_gid and end_word_counts[t["word"].lower()] < 2: open_out.append({"l": i, "s": t["start"], "e": t["end"]}) return {"lines": lines, "tokens": toks_out, "groups": groups_out, diff --git a/tests/test_rhymes.py b/tests/test_rhymes.py index 5c7a313..5a97532 100644 --- a/tests/test_rhymes.py +++ b/tests/test_rhymes.py @@ -599,3 +599,18 @@ def test_repeated_this_joins_the_hook_chain(): "flickin' that wrist\n" "now they fuckin' with this") group_with(text, "shit", "assist", "wrist", "this") + + +def test_repetition_alone_does_not_color(): + text = ("Six-foot, seven-foot, eight-foot bunch\n" + "Six-foot, seven-foot, eight-foot bunch") + assert highlighted(text) == set() + assert scheme(text) == "aa" # ...but refrains share a scheme letter + # and a repeated unrhymed ending isn't flagged "unanswered" either + res = analyze(Draft(text=text)) + assert res["open"] == [] + + +def test_repetition_colors_once_a_differing_word_joins(): + text = "it was Tammy\npure whammy\nstill Tammy" + group_with(text, "tammy", "whammy")