mirror of
https://github.com/kennethreitz/rhymepad.org.git
synced 2026-06-11 17:08:33 +00:00
Coda-nesting fusion, s/z neutralization, wrapping draft tabs
- Single-vowel perfect families whose coda classes nest fuse into one chain: wrist (IH S T) hubs this (IH S) and shit (IH T), which ride as slant satellites — Logic's hook reads as one color - Final s/z voicing neutralizes in coda classes and weak-ending keys: vamonos rhymes dominoes - The drafts bar wraps instead of scrolling, so many drafts stay visible Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -219,7 +219,8 @@ def founding_projections(key: str) -> dict[str, str]:
|
||||
# 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:])
|
||||
out["weak"] = "w:" + " ".join(
|
||||
[ph[i]] + [_coda_class(c) for c in ph[i + 1:]])
|
||||
break
|
||||
elif key.startswith("v:"): # vowel tail
|
||||
out["slant"] = key
|
||||
@@ -288,8 +289,11 @@ NASALS = {"M", "N", "NG"}
|
||||
|
||||
|
||||
def _coda_class(c: str) -> str:
|
||||
"""damn/hand/plans and time/line ride one nasal class in delivery."""
|
||||
return "N" if c in NASALS else c
|
||||
"""damn/hand/plans ride one nasal class in delivery; final s/z
|
||||
voicing neutralizes too (vamonos/dominoes)."""
|
||||
if c in NASALS:
|
||||
return "N"
|
||||
return "S" if c == "Z" else c
|
||||
|
||||
|
||||
def vc_key(word: str) -> str | None:
|
||||
@@ -365,7 +369,9 @@ def weak_end_key(word: str) -> str | 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:]))
|
||||
syl = [DIGITS.sub("", p) for p in pl[i:]]
|
||||
out = [syl[0]] + [_coda_class(c) for c in syl[1:]]
|
||||
return "w:" + " ".join(out)
|
||||
return None
|
||||
|
||||
|
||||
@@ -908,6 +914,67 @@ def analyze(draft: Draft):
|
||||
tgt["toks"].extend(g["toks"])
|
||||
raw_groups = fused
|
||||
|
||||
# fuse single-vowel perfect families whose coda classes NEST — the
|
||||
# hook chain: wrist (IH S T) is the hub that pulls in this (IH S, a
|
||||
# prefix) and shit (IH T, a suffix). Same-vowel families with nested
|
||||
# codas read as one chain in delivery; absorbed members mark slant.
|
||||
def _sv_parse(g):
|
||||
key = g["key"]
|
||||
if not key.startswith("p:"):
|
||||
return None
|
||||
ph = key[2:].split()
|
||||
if not ph or ph[0] not in ARPA_VOWELS:
|
||||
return None
|
||||
if any(p in ARPA_VOWELS for p in ph[1:]):
|
||||
return None
|
||||
coda = tuple(_coda_class(c) for c in ph[1:])
|
||||
return (ph[0], coda) if coda else None
|
||||
|
||||
sv = [(gi, p) for gi, p in ((gi, _sv_parse(g))
|
||||
for gi, g in enumerate(raw_groups)) if p]
|
||||
parent = list(range(len(raw_groups)))
|
||||
|
||||
def find(i):
|
||||
while parent[i] != i:
|
||||
parent[i] = parent[parent[i]]
|
||||
i = parent[i]
|
||||
return i
|
||||
|
||||
for ai in range(len(sv)):
|
||||
for bi in range(ai + 1, len(sv)):
|
||||
gi, (va, ca) = sv[ai]
|
||||
gj, (vb, cb) = sv[bi]
|
||||
if va != vb or ca == cb:
|
||||
continue
|
||||
s, l = (ca, cb) if len(ca) < len(cb) else (cb, ca)
|
||||
if l[:len(s)] == s or l[len(l) - len(s):] == s:
|
||||
parent[find(gi)] = find(gj)
|
||||
|
||||
clusters = defaultdict(list)
|
||||
for gi in range(len(raw_groups)):
|
||||
clusters[find(gi)].append(gi)
|
||||
if any(len(m) > 1 for m in clusters.values()):
|
||||
def _coda_len(gi):
|
||||
p = _sv_parse(raw_groups[gi])
|
||||
return len(p[1]) if p else 0
|
||||
fused2 = []
|
||||
for members in clusters.values():
|
||||
if len(members) == 1:
|
||||
fused2.append(raw_groups[members[0]])
|
||||
continue
|
||||
hub = max(members,
|
||||
key=lambda gi: (_coda_len(gi),
|
||||
len(raw_groups[gi]["toks"])))
|
||||
core = raw_groups[hub]
|
||||
for gi in members:
|
||||
if gi == hub:
|
||||
continue
|
||||
for t in raw_groups[gi]["toks"]:
|
||||
t["slant"] = True
|
||||
core["toks"].extend(raw_groups[gi]["toks"])
|
||||
fused2.append(core)
|
||||
raw_groups = fused2
|
||||
|
||||
# stable colors: order groups by first appearance
|
||||
raw_groups.sort(key=lambda g: min((t["line"], t["start"]) for t in g["toks"]))
|
||||
groups_out = []
|
||||
|
||||
+1
-1
@@ -69,7 +69,7 @@
|
||||
|
||||
/* ---- editor side ---- */
|
||||
.editor-col { display: flex; flex-direction: column; min-height: 0; gap: 10px; }
|
||||
.drafts-bar { display: flex; gap: 6px; overflow-x: auto; flex: none; padding-bottom: 2px; }
|
||||
.drafts-bar { display: flex; flex-wrap: wrap; gap: 6px; flex: none; padding-bottom: 2px; }
|
||||
.dtab {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 12px; padding: 6px 12px; max-width: 170px;
|
||||
|
||||
@@ -567,3 +567,21 @@ def test_unanswered_endings_reported():
|
||||
opens = {res["lines"][o["l"]][o["s"]:o["e"]] for o in res["open"]}
|
||||
assert "blue" in opens and "end" in opens
|
||||
assert "hat" not in opens and "cat" not in opens
|
||||
|
||||
|
||||
def test_hook_coda_chain_fuses():
|
||||
# Logic's hook: wrist (IH S T) is the hub; this (IH S) and shit
|
||||
# (IH T) ride it as slant satellites — one color, kinda-rhymes kept
|
||||
text = ("Yeah I've been killin' this shit\n"
|
||||
"Yeah I've been hard in the paint, not a single assist\n"
|
||||
"Yeah I've been flickin' that wrist\n"
|
||||
"Yeah I've been cookin' that shit, now they fuckin' with this")
|
||||
group_with(text, "shit", "assist", "wrist", "this")
|
||||
|
||||
|
||||
def test_vamonos_dominoes():
|
||||
# final s/z voicing neutralizes: -nos rhymes -noes
|
||||
text = ("my members go quicker than vamonos\n"
|
||||
"He dead, she dead, he in jail\n"
|
||||
"Everyone fallin' like dominoes")
|
||||
group_with(text, "vamonos", "dominoes")
|
||||
|
||||
Reference in New Issue
Block a user