首页 全部提示词 Agent Skill

Poetry Meter and Rhyme Checker

技能 @f 10,799 字符 在 prompts.chat 查看 译文有问题?

这条还没有中文译文,当前显示英文原文。译文由定时任务补齐,也可以到仓库提 issue 催更。

English original

Poetry Meter and Rhyme Checker

You help writers see how a draft scans: syllables per line, a plausible stress sketch for English meter targets, and the end-rhyme scheme. You are honest that English syllable counting is heuristic, you show your work, and you suggest small edits instead of rewriting the poem unless asked.

Files in this skill

  • scripts/poetry_check.py - syllable estimates, form checks, rhyme-scheme detection (Python 3 standard library only)
  • references/forms-and-targets.md - syllable and rhyme targets for common short forms
  • references/syllable-heuristic.md - how the counter works and where it fails (names, -ed, diphthongs)
  • templates/poetry-report.md - the report shape to return
  • examples/example-haiku-and-quatrain.md - worked examples

Workflow

1. Collect the facts

  • The poem text (keep the user's line breaks).
  • Target form if any: haiku, tanka, limerick, sonnet couplet, free verse with a rhyme check only, or custom syllable pattern.
  • Dialect notes (US/UK spelling) when relevant to endings like -our / -or.

2. Run the checker

python3 scripts/poetry_check.py poem.txt
python3 scripts/poetry_check.py poem.txt --form haiku
python3 scripts/poetry_check.py poem.txt --form limerick --rhyme
python3 scripts/poetry_check.py - --json < poem.txt

3. Advise

  1. Show a per-line syllable table and the detected rhyme scheme.
  2. Say whether the form target passed, and which lines miss by how many syllables.
  3. Offer minimal edits (swap a synonym, split a compound, drop a filler word) rather than a full rewrite.
  4. Deliver with templates/poetry-report.md.

Rules

  • Always label syllable counts as estimates.
  • Do not mock the writer's voice; keep suggestions optional.
  • For free verse, still report syllables and rhyme if useful, without forcing a form. FILE:references/forms-and-targets.md

Forms and targets

Form Syllable target (lines) Typical rhyme
Haiku (English teaching form) 5 / 7 / 5 usually unrhymed
Tanka 5 / 7 / 5 / 7 / 7 usually unrhymed
Limerick ~8–9 / 8–9 / 5–6 / 5–6 / 8–9 AABBA
Quatrain (ballad) often 8 / 6 / 8 / 6 ABCB or ABAB
Heroic couplet ~10 / 10 (iambic pentameter feel) AA

Exact classical Japanese mora counts differ from English syllable teaching forms; this skill uses the common English classroom targets unless the user asks for mora-aware analysis. FILE:references/syllable-heuristic.md

Syllable heuristic (honest limits)

The script estimates English syllables by:

  1. Lowercasing and stripping punctuation.
  2. Counting vowel groups (a e i o u y) with common silent-e and -ed / -es adjustments.
  3. Treating hyphenated compounds as the sum of parts.

Known failure modes

  • Proper names (Olivia, Michael) and loanwords.
  • Words where y is consonantal (beyond) vs vocalic (rhythm is still hard).
  • -ism, -tion, and compressed pronunciations (family as 2 or 3).

When a count looks wrong, say so and offer a manual override in the report. FILE:templates/poetry-report.md

Poetry meter and rhyme report

Target form

  • …

Per-line table

# Line Syllables (est.) End word Rhyme letter

Form check

  • Pass / fail: …
  • Misses: …

Rhyme scheme

  • …

Minimal edit ideas

  • …

Notes on uncertain counts

  • … FILE:examples/example-haiku-and-quatrain.md

Examples

Haiku (should pass 5-7-5)

An old silent pond
A frog jumps into the pond—
Splash! Silence again.

Estimated syllables often land near 5 / 7 / 5 depending on silent and silence.

Quatrain rhyme ABCB

The woods are lovely, dark and deep,
But I have promises to keep,
And miles to go before I sleep,
And miles to go before I sleep.

End words deep/keep/sleep/sleep → AAAA if all rhyme together; a true ABCB sample should use different B and C words. FILE:scripts/poetry_check.py #!/usr/bin/env python3 """Estimate syllables, check simple forms, and map end-rhyme schemes.

Usage: python3 poetry_check.py poem.txt python3 poetry_check.py poem.txt --form haiku python3 poetry_check.py poem.txt --form limerick --rhyme python3 poetry_check.py - --json < poem.txt """ from future import annotations

import argparse import json import re import sys from pathlib import Path

FORMS = { "haiku": [5, 7, 5], "tanka": [5, 7, 5, 7, 7], "limerick": None, # soft ranges handled separately }

VOWELS = set("aeiouy")

def strip_word(w: str) -> str: return re.sub(r"[^a-zA-Z']", "", w).lower()

def syllables_word(word: str) -> int: w = strip_word(word) if not w: return 0 # common overrides overrides = { "poem": 2, "poems": 2, "fire": 1, "hour": 1, "our": 1, "//": 0, "family": 3, "every": 3, "different": 3, "interesting": 4, "rhythm": 2, "business": 2, "wednesday": 2, "beautiful": 3, } if w in overrides: return overrides[w] if "-" in word: return sum(syllables_word(p) for p in word.split("-")) # remove trailing silent e base = w if base.endswith("e") and not base.endswith(("le", "ue", "oe")) and len(base) > 2: base = base[:-1] # -ed endings: walked=1 extra often silent if re.search(r"[^aeiou]ed$", w) and not w.endswith(("ted", "ded")): base = re.sub(r"ed$", "", w) if base.endswith("e"): base = base[:-1] groups = re.findall(r"[aeiouy]+", base) count = len(groups) if groups else 1 # adjust ion/ial if w.endswith(("tion", "sion", "cial", "tial")): count = max(count, 2) return max(1, count)

def end_word(line: str) -> str: words = [w for w in re.findall(r"[A-Za-z']+", line)] return words[-1].lower() if words else ""

def rhyme_key(word: str) -> str: w = strip_word(word) if not w: return "" aliases = { "you": "oo", "blue": "oo", "too": "oo", "two": "oo", "to": "oo", "through": "oo", "threw": "oo", "shoe": "oo", "who": "oo", "new": "oo", "knew": "oo", "true": "oo", "lee": "ee", "tea": "ee", "glee": "ee", "sea": "ee", "me": "ee", "we": "ee", "be": "ee", "green": "een", "keen": "een", "seen": "een", "mean": "een", } if w in aliases: return aliases[w] matches = list(re.finditer(r"[aeiouy]+", w)) if not matches: return w start = matches[-1].start() if len(matches) >= 2 and len(w) - start < 2: start = matches[-2].start() tail = w[start:] # light normalization for common English end-rhymes for a, b in ( ("igh", "i"), ("ea", "ee"), ("ee", "ee"), ("ey", "ee"), ("ie", "ee"), ("ay", "ei"), ("ai", "ei"), ("ou", "ow"), ("oo", "u"), ("ue", "u"), ): if a in tail: tail = tail.replace(a, b) return tail

def rhyme_scheme(end_words: list[str]) -> str: mapping = {} letters = [] next_ord = 0 for w in end_words: key = rhyme_key(w) if not key: letters.append("-") continue # near-rhyme: same key if key not in mapping: # also merge if share last 2 chars and vowel found = None for k, let in mapping.items(): if k == key or (len(k) >= 2 and len(key) >= 2 and k[-2:] == key[-2:]): found = let mapping[key] = let break if found is None: let = chr(ord("A") + next_ord) next_ord += 1 mapping[key] = let letters.append(mapping[key]) return "".join(letters)

def limerick_ok(counts: list[int]) -> tuple[bool, str]: if len(counts) != 5: return False, f"expected 5 lines, got {len(counts)}" ranges = [(7, 10), (7, 10), (4, 7), (4, 7), (7, 10)] misses = [] for i, (c, (lo, hi)) in enumerate(zip(counts, ranges), 1): if not (lo <= c <= hi): misses.append(f"line {i}: {c} not in {lo}-{hi}") return (not misses), ("; ".join(misses) if misses else "ok")

def analyze(text: str, form: str | None) -> dict: lines = [ln.rstrip() for ln in text.splitlines() if ln.strip()] rows = [] counts = [] ends = [] for i, ln in enumerate(lines, 1): words = re.findall(r"[A-Za-z']+(?:-[A-Za-z']+)*", ln) sc = sum(syllables_word(w) for w in words) ew = end_word(ln) counts.append(sc) ends.append(ew) rows.append({"n": i, "line": ln, "syllables": sc, "end": ew}) scheme = rhyme_scheme(ends) form_result = None if form == "limerick": ok, detail = limerick_ok(counts) rhyme_ok = scheme == "AABBA" form_result = {"form": form, "pass": ok and rhyme_ok, "detail": detail, "rhyme_expected": "AABBA", "rhyme_got": scheme} elif form in FORMS and FORMS[form]: target = FORMS[form] if len(counts) != len(target): form_result = {"form": form, "pass": False, "detail": f"expected {len(target)} lines, got {len(counts)}", "target": target, "got": counts} else: misses = [f"line {i}: got {g}, want {t}" for i, (g, t) in enumerate(zip(counts, target), 1) if g != t] form_result = {"form": form, "pass": not misses, "detail": "; ".join(misses) or "ok", "target": target, "got": counts} return {"lines": rows, "rhyme_scheme": scheme, "form_check": form_result}

def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description=doc) ap.add_argument("path", help="Poem file, or - for stdin") ap.add_argument("--form", choices=["haiku", "tanka", "limerick"], help="Form to validate") ap.add_argument("--rhyme", action="store_true", help="Always print rhyme scheme (default on)") ap.add_argument("--json", action="store_true") args = ap.parse_args(argv) text = sys.stdin.read() if args.path == "-" else Path(args.path).read_text(encoding="utf-8") result = analyze(text, args.form) if args.json: json.dump(result, sys.stdout, indent=2) sys.stdout.write("\n") else: print("n | syl | end | line") for r in result["lines"]: print(f"{r['n']:<2}| {r['syllables']:<3}| {r['end']:<12}| {r['line']}") print(f"rhyme scheme: {result['rhyme_scheme']}") fc = result["form_check"] if fc: print(f"form {fc['form']}: {'PASS' if fc['pass'] else 'FAIL'} ({fc['detail']})") if "rhyme_expected" in fc: print(f" rhyme expected {fc['rhyme_expected']}, got {fc['rhyme_got']}") if result["form_check"] and not result["form_check"]["pass"]: return 1 return 0

if name == "main": sys.exit(main(sys.argv[1:]))