From cab28ed23e34703b0d7fd25ebc8700cf530b995f Mon Sep 17 00:00:00 2001 From: Kenneth Reitz Date: Sun, 7 Jun 2026 02:57:31 -0400 Subject: [PATCH] Mosaic triple rhymes + UI truncation Three-word vowel mosaics (mean to it / seen do it / theme music) now group, gated hard: the mosaic must draw a full vowel beyond its anchor word, and all-phrase buckets mirroring the same two word groups stay suppressed. Scheme readout truncates at 16 letters; draft tabs cap at 170px with ellipsis. Co-Authored-By: Claude Opus 4.8 (1M context) --- app.py | 60 +++++++++++++++++++++++++++++++++++++++++--- static/index.html | 11 +++++--- tests/test_rhymes.py | 9 +++++++ 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/app.py b/app.py index 5484566..b36a008 100644 --- a/app.py +++ b/app.py @@ -448,6 +448,40 @@ def analyze(draft: Draft): or b["word"].lower() in STOPWORDS), }) + # mosaic triples: three-word runs whose vowel run is the rhyme — + # "mean to it" / "seen do it" / "theme music" (IY UW x). Anchor must + # carry content; the tail words may be anything pronounceable. + for toks in line_toks.values(): + for a, b, c in zip(toks, toks[1:], toks[2:]): + if a["word"].lower() in STOPWORDS: + continue + pa, pb, pc = (phones_for(a["word"]), phones_for(b["word"]), + phones_for(c["word"])) + if not (pa and pb and pc): + continue + tail_vowels = _all_vowels(pb) + _all_vowels(pc) + if not any(v not in REDUCED for v in tail_vowels): + continue # the mosaic must span words: "mean TO it" does, + # "methamphetamine with the" is just its anchor + vowels = _tail_vowels(pa) + tail_vowels + if len(vowels) < 3: + continue + pl = pa.split() + start = 0 + for i in range(len(pl) - 1, -1, -1): + if pl[i][-1] in "12": + start = i + break + phrases.append({ + "line": a["line"], "start": a["start"], "end": c["end"], + "word": " ".join(w["word"].lower() for w in (a, b, c)), + "is_end": c["is_end"], "sid": a["sid"], "gid": None, + "slant": False, "vowels": vowels, + "rime": DIGITS.sub( + "", " ".join(pl[start:] + pb.split() + pc.split())), + "weak": False, + }) + # pass 1: perfect rhymes (shared rime), anywhere in a line — this is # what catches internal rhymes. Phrases compete too, so "stir up" # perfect-rhymes "syrup" even while its "up" rhymes with "cup". @@ -573,11 +607,17 @@ def analyze(draft: Draft): key = _m2_key(p["rime"].split()) if not key: continue + if p["word"].count(" ") == 2: + # mosaic triples must carry at least two full vowels — + # anchor + schwa-tails ("Sleep is the") prove nothing + if sum(v != "x" for v in key[2:].split()) < 2: + continue spans = grouped_spans[p["line"]] a_gi = next((gi for s, e, gi in spans if s <= p["start"] < e), None) b_gi = next((gi for s, e, gi in spans if s < p["end"] <= e), None) + p["halves"] = (a_gi, b_gi) if a_gi is not None and b_gi is not None: - # both halves already rhyme — the phrase only matters if it + # both ends already rhyme — the phrase only matters if it # ties into a THIRD family (four-inch joining the orange/ # storage clan while four sits with door and inch with hinge) gi = group_by_multi.get((p["sid"], key)) @@ -585,6 +625,10 @@ def analyze(draft: Draft): raw_groups[gi]["toks"].append(p) p["slant"] = True grouped.add(id(p)) + else: + # or if it can seed a family with non-mirror siblings + # (mean to it / seen do it / theme music) + by_multi[(p["sid"], key)].append(p) continue attach_or_collect(p, key, by_multi, group_by_multi) @@ -594,9 +638,17 @@ def analyze(draft: Draft): for (sid, key), toks in sorted(by_multi.items(), key=lambda kv: (-len(kv[1]), kv[0][1])): toks = [t for t in toks if id(t) not in grouped] - if len(toks) >= 2 and len({t["word"].split()[0] for t in toks}) >= 2: - raw_groups.append({"toks": toks, "slant": True, "key": key}) - grouped.update(id(t) for t in toks) + if len(toks) < 2 or len({t["word"].split()[0] for t in toks}) < 2: + continue + # an all-phrase bucket whose members mirror the same two word + # groups is pure redundancy (oh my / go rhyme over oh+go, my+rhyme) + halves = {t.get("halves") for t in toks} + if (len(halves) == 1 + and None not in halves + and None not in next(iter(halves))): + continue + raw_groups.append({"toks": toks, "slant": True, "key": key}) + grouped.update(id(t) for t in toks) # pass 4: consonance-aware slant anywhere in a line — last stressed # vowel + first coda consonant, so bliss / whisps / exist (IH S) group diff --git a/static/index.html b/static/index.html index 2234500..3b338e1 100644 --- a/static/index.html +++ b/static/index.html @@ -65,11 +65,12 @@ .drafts-bar { display: flex; gap: 6px; overflow-x: auto; flex: none; padding-bottom: 2px; } .dtab { display: flex; align-items: center; gap: 8px; - font-size: 12px; padding: 6px 12px; + font-size: 12px; padding: 6px 12px; max-width: 170px; background: var(--panel-2); border: 1px solid var(--line); border-radius: 8px; color: var(--ink-dim); cursor: pointer; white-space: nowrap; - transition: all .15s; user-select: none; + transition: all .15s; user-select: none; flex: none; } + .dtab .dtitle { overflow: hidden; text-overflow: ellipsis; } .dtab:hover { color: var(--ink); } .dtab.active { color: var(--ink); border-color: var(--accent); background: rgba(232,129,74,0.08); } .dtab .x { color: var(--ink-dim); font-size: 13px; line-height: 1; } @@ -511,7 +512,11 @@ function buildReadout(){ parts.push(p); } const st = caretStanza(); - if(st && st.scheme) parts.push(`scheme ${st.scheme.toUpperCase().split('').join(' ')}`); + if(st && st.scheme){ + const sch = st.scheme.toUpperCase(); + const shown = sch.slice(0, 16).split('').join(' ') + (sch.length > 16 ? ' …' : ''); + parts.push(`scheme ${shown}`); + } schemeReadout.innerHTML = parts.join('    '); } diff --git a/tests/test_rhymes.py b/tests/test_rhymes.py index f0753de..e6724c1 100644 --- a/tests/test_rhymes.py +++ b/tests/test_rhymes.py @@ -366,3 +366,12 @@ def test_the_full_orange_verse(): "four-inch", "door hinge") group_with(text, "four", "door", "george") group_with(text, "inch", "hinge") + + +def test_mosaic_triples_kanye_power(): + text = ("I'm living in that 21st century\n" + "Doing something mean to it\n" + "Do it better than anybody you ever seen do it\n" + "Screams from the haters, got a nice ring to it\n" + "I guess every superhero need his theme music") + group_with(text, "mean to it", "seen do it", "theme music")