mirror of
https://github.com/kennethreitz/rhymepad.org.git
synced 2026-06-11 17:08:33 +00:00
Meter coaching: flag lines that break the stanza's pattern
When >=60% of a stanza's lines share a syllable count (+-1), outliers get a wavy underline and the readout names the target count. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -540,13 +540,31 @@ def analyze(draft: Draft):
|
||||
"slant": t["slant"] or groups_out[t["gid"]]["slant"]}
|
||||
for t in [*tokens, *phrases] if t["gid"] is not None
|
||||
]
|
||||
meter = []
|
||||
meter, meter_by_line = [], {}
|
||||
for i, line in enumerate(lines):
|
||||
if sids[i] is None:
|
||||
continue
|
||||
m = line_meter(line)
|
||||
if m:
|
||||
meter.append({"l": i, **m})
|
||||
entry = {"l": i, **m, "off": False}
|
||||
meter.append(entry)
|
||||
meter_by_line[i] = entry
|
||||
|
||||
# meter coaching: when a stanza has a clear syllable pattern, flag
|
||||
# the lines that break it
|
||||
for s, lns in stanza_lines.items():
|
||||
entries = [meter_by_line[i] for i in lns if i in meter_by_line]
|
||||
if len(entries) < 3:
|
||||
continue
|
||||
counts = [e["syl"] for e in entries]
|
||||
# mode with +-1 tolerance: the count that covers the most lines
|
||||
mode = max(set(counts), key=lambda c: sum(abs(v - c) <= 1 for v in counts))
|
||||
covered = sum(abs(v - mode) <= 1 for v in counts)
|
||||
if covered / len(entries) >= 0.6:
|
||||
for e in entries:
|
||||
e["off"] = abs(e["syl"] - mode) > 1
|
||||
for e in entries:
|
||||
e["target"] = mode
|
||||
|
||||
return {"lines": lines, "tokens": toks_out,
|
||||
"groups": groups_out, "stanzas": stanzas, "meter": meter}
|
||||
|
||||
+12
-2
@@ -124,6 +124,11 @@
|
||||
.rh.slant.end {
|
||||
box-shadow: inset 0 -2px 0 0 color-mix(in srgb, var(--c) 45%, transparent);
|
||||
}
|
||||
.offbeat {
|
||||
text-decoration: underline wavy color-mix(in srgb, var(--r6) 45%, transparent);
|
||||
text-decoration-skip-ink: none;
|
||||
text-underline-offset: 7px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
|
||||
}
|
||||
@@ -494,7 +499,8 @@ function render(){
|
||||
pos = t.e;
|
||||
});
|
||||
h += esc(line.slice(pos));
|
||||
html += h + '\n';
|
||||
const m = fresh && analysis.meter ? analysis.meter.find(x=>x.l===i) : null;
|
||||
html += (m && m.off ? `<span class="offbeat">${h}</span>` : h) + '\n';
|
||||
});
|
||||
highlight.innerHTML = html;
|
||||
highlight.scrollTop = editor.scrollTop;
|
||||
@@ -522,7 +528,11 @@ function buildReadout(){
|
||||
const parts = [];
|
||||
const ln = caretLine();
|
||||
const m = analysis && analysis.meter ? analysis.meter.find(x=>x.l===ln) : null;
|
||||
if(m) parts.push(`${m.syl} syl` + (m.label ? ` · ${m.label}` : ''));
|
||||
if(m){
|
||||
let p = `${m.syl} syl` + (m.label ? ` · ${m.label}` : '');
|
||||
if(m.off) p += ` <span class="offline">· breaks stanza meter (~${m.target} syl)</span>`;
|
||||
parts.push(p);
|
||||
}
|
||||
const st = caretStanza();
|
||||
if(st && st.scheme) parts.push(`scheme <b>${st.scheme.toUpperCase().split('').join(' ')}</b>`);
|
||||
schemeReadout.innerHTML = parts.join(' ');
|
||||
|
||||
@@ -222,3 +222,20 @@ def test_trailing_schwa_trimmed_militia_commissioner():
|
||||
text = ("Swagger down pat, call my shit Patricia\n"
|
||||
"Young Money militia, and I am the commissioner")
|
||||
group_with(text, "patricia", "militia", "commissioner")
|
||||
|
||||
|
||||
def test_meter_coaching_flags_outlier_line():
|
||||
text = ("I walk the lonely road tonight\n"
|
||||
"I hold the heavy stone of light\n"
|
||||
"I call the fading stars to fight\n"
|
||||
"and everybody wonders where the time has gone")
|
||||
res = analyze(Draft(text=text))
|
||||
flagged = [m["l"] for m in res["meter"] if m["off"]]
|
||||
assert flagged == [3]
|
||||
assert res["meter"][3]["target"] == 8
|
||||
|
||||
|
||||
def test_meter_coaching_needs_a_pattern_to_break():
|
||||
# two lines of wildly different lengths: no dominant pattern, no flags
|
||||
res = analyze(Draft(text="short line here\na very much longer line that runs on and on"))
|
||||
assert not any(m["off"] for m in res["meter"])
|
||||
|
||||
Reference in New Issue
Block a user