
Lyric Diagnostic
- 350 installs
- 133 repo stars
- Updated February 24, 2026
- jwynia/agent-skills
Diagnose weak song lyrics: scan rhyme, meter, clichés, narrative clarity, and emotional arc, then suggest targeted rewrites before publishing or performing.
About
Diagnostic skill for song lyrics: evaluates rhyme schemes, rhythm, clichés, story clarity, and emotional payoff, then proposes concrete rewrites so verses feel intentional and performance-ready.
- Rhyme and meter checks
- Cliché detection
- Narrative arc review
- Singability passes
- Targeted rewrite suggestions
Lyric Diagnostic by the numbers
- 350 all-time installs (skills.sh)
- +6 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #472 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jwynia/agent-skills --skill lyric-diagnosticAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 350 |
|---|---|
| repo stars | ★ 133 |
| Last updated | February 24, 2026 |
| Repository | jwynia/agent-skills ↗ |
What it does
Diagnose weak song lyrics: scan rhyme, meter, clichés, narrative clarity, and emotional arc, then suggest targeted rewrites before publishing or performing.
Files
Lyric Diagnostic: Skill
You diagnose lyric problems across multiple dimensions. Your role is to identify why lyrics fail and guide writers (human or AI) toward more effective choices.
Core Principle
Lyrics fail in predictable, diagnosable ways. Generic is not neutral—generic is bad.
AI-generated lyrics cluster at statistical distribution centers: the most probable word choices, rhyme pairs, and emotional vocabulary. Probability optimization produces mediocrity. Effective lyrics require intentional deviation from defaults.
The same pattern that makes "Celtic" trigger tinwhistle in music generation makes "love" trigger predictable rhyme pairs and emotional cliches in lyric generation.
---
The Lyric States
State LY1: Cliche Trap
Symptoms: Predictable rhyme pairs dominate. Word choices are first-associations. Nothing surprises. Lyrics could fit any song on this topic.
Key Questions:
- What rhyme pairs are you using? Are they first-associations?
- Would replacing the end-rhyme word break the meaning? (If not, it's filler)
- Can you enumerate the default words for this topic and avoid them?
- What would a different genre use for this same emotion?
Diagnostic Checklist:
- [ ] No Tier 1 cliche words (love, heart, baby, forever)
- [ ] No stock phrases (see list below)
- [ ] Rhyme pairs are not obvious first-associations
- [ ] At least one surprising word choice per verse
- [ ] Specific details rather than generic statements
Stock Phrases (phrase-level cliches, not just words):
Scripts can't catch these—they require semantic judgment:
| Phrase | Category | Why It's Cliche |
|---|---|---|
| "screaming inside" | unexpressed anguish | Every song about hidden pain uses this |
| "need you gone" | direct desire | States want instead of showing it |
| "won't leave me alone" | persistence | Generic memory/thought complaint |
| "thought I died" | emotional death metaphor | Overused intensity marker |
| "used to be" + past state | nostalgia setup | Stock before/after framing |
| "look in your eyes" | connection | Vague romantic gesture |
| "torn apart" | destruction | Abstract violence metaphor |
| "can't let go" | attachment | States the problem directly |
Lazy Rhyme Pairs (rhyme drove word choice):
| Pair | Problem |
|---|---|
| desire / fire / tired | Cluster appears together constantly |
| inside / died / alive | Death-interior cluster |
| away / stay / day | Motion-time cluster |
| heart / apart / start | Heart-destruction cluster |
| alone / gone / on | Isolation cluster |
Interventions:
- Run
cliche-check.tsas pre-filter (surfaces candidates, doesn't diagnose) - Scan for stock phrases manually—scripts miss these
- List the 10 most predictable rhymes for your end words—avoid them all
- Replace abstract emotions with concrete sensory imagery
- Ask: "What would someone who hates this genre never say?" Use that.
---
State LY2: Meter Mismatch
Symptoms: Lines don't fit the melody. Stresses fall on wrong syllables. Unnatural pronunciation required. Syllables crammed or stretched awkwardly.
Key Questions:
- Can you tap the rhythm and speak the line simultaneously?
- Where do stresses fall? Do they match the melody's emphasis?
- Are any syllables swallowed or stretched unnaturally?
- How many syllables per phrase? Is there consistency?
Diagnostic Checklist:
- [ ] Natural word stress aligns with musical beats
- [ ] Syllable counts fit the melodic phrase
- [ ] No awkward pronunciations required
- [ ] Contractions/elisions are singable
Interventions:
- Run
meter-check.tsto analyze syllable counts and stress patterns - Speak the lyrics while tapping the beat—mismatches become obvious
- Swap words with same meaning but different syllable count
- Use contractions to reduce syllables, or expand to add them
---
State LY3: Shallow Surface
Symptoms: Lyrics say exactly what they mean and nothing more. No subtext, no layers, no depth. Emotions are stated directly. Everything is on the surface.
Key Questions:
- Is there a second reading? Something between the lines?
- Are you stating the emotion or evoking it through imagery?
- What specific concrete image grounds the abstraction?
- Could this be understood on first listen but reveal depth on repeat?
Diagnostic Checklist:
- [ ] At least one image per verse that works on multiple levels
- [ ] Emotions evoked through detail, not stated directly
- [ ] Something for repeat listens to discover
- [ ] Concrete specifics anchor abstract concepts
Interventions:
- For every abstract statement, find a concrete action/image that embodies it
- Instead of "I'm sad," show what sad looks like, sounds like, feels like
- Add a detail that doesn't quite fit—creates productive ambiguity
- Test: can you explain two different interpretations of the same line?
---
State LY4: Register Mismatch
Symptoms: Lyric tone contradicts the music's emotion. Casual words over dramatic instrumentation. Heavy lyrics over light music. Vocabulary doesn't match genre expectations.
Key Questions:
- What emotion should the listener feel at this moment?
- Is your vocabulary elevated/casual matching the music?
- Does the distance (intimate/observational) match the instrumental?
- Would this language make sense in this genre?
Diagnostic Checklist:
- [ ] Vocabulary matches musical intensity
- [ ] Distance (I/you vs. they/we) fits the sound
- [ ] Genre vocabulary expectations met or intentionally broken
- [ ] Irony (if present) is signaled clearly
Interventions:
- Listen to the music without lyrics—what emotions does it evoke?
- Match lyric intensity to musical intensity section by section
- Check genre vocabulary norms—violate intentionally or not at all
- If using irony, add signal words or structural cues
---
State LY5: Sound Neglect
Symptoms: No attention to how words feel in the mouth. Harsh consonant clusters at tender moments. Monotonous vowel sounds. No alliteration, assonance, or deliberate sound texture.
Key Questions:
- What consonant sounds dominate? What texture do they create?
- Do vowel sounds support the emotional content?
- Are there harsh clusters that disrupt flow?
- Is there intentional sound painting?
Diagnostic Checklist:
- [ ] Soft consonants (l, m, n, w) at intimate moments
- [ ] Hard consonants (k, t, p) at impactful moments
- [ ] Vowel sounds support emotional content
- [ ] At least one sound device per verse (alliteration, assonance)
Interventions:
- Read lyrics aloud focusing only on how they feel in the mouth
- Map emotional moments to appropriate sound textures
- Replace harsh-sounding words at tender moments
- Add deliberate alliteration or assonance at key phrases
---
State LY6: Structural Chaos
Symptoms: No identifiable hook. Verse and chorus feel interchangeable. Repetition is annoying, not reinforcing. Bridge doesn't contrast. Song has no arc.
Key Questions:
- Is there a clear hook? Where does it land?
- Does each verse add new information or repeat?
- Does the bridge actually contrast or just continue?
- Is repetition earning its place?
Diagnostic Checklist:
- [ ] Hook is identifiable and memorable
- [ ] Verses differentiate (new info, different angle)
- [ ] Chorus delivers emotional payoff
- [ ] Bridge provides contrast or escalation
- [ ] Repetition evolves or emphasizes, doesn't bore
Structural Cliches:
| Pattern | Problem | Fix |
|---|---|---|
| Bridge = Verse | Not a bridge at all—just more of the same | Shift perspective, time, or speaker |
| Chorus expands on repeat | Adding lines without development | Each chorus iteration should mean more |
| Hook repeated without exploration | Says the phrase but never unpacks it | What does the hook mean? Show us. |
| No arc | Emotional state same at end as beginning | Something must change—realization, acceptance, escalation |
| Verses say same thing differently | Verse 2 is Verse 1 with synonyms | Each verse needs new information or angle |
Interventions:
- Identify the hook—if you can't, create one
- Make each verse answer a different question about the topic
- Bridge should shift perspective, time, or intensity
- Cut repetition that doesn't earn its place
- Check: is the speaker in a different place emotionally at the end?
---
Diagnostic Process
Step 1: First Listen (Holistic)
Listen without analyzing. Note:
- What's the overall impression?
- What lines feel strong or weak?
- Where does attention wander?
- What's memorable after one pass?
Step 2: Identify Primary State
Which of the six states best describes the main problem?
- LY1: Feels generic/predictable → Cliche Trap
- LY2: Doesn't fit the melody → Meter Mismatch
- LY3: Says nothing deep → Shallow Surface
- LY4: Tone feels wrong → Register Mismatch
- LY5: Sounds awkward → Sound Neglect
- LY6: No structure/hook → Structural Chaos
Step 3: Run Diagnostics
For the primary state:
- Answer the key questions
- Run relevant scripts (
cliche-check.ts,meter-check.ts, etc.) - Complete the diagnostic checklist
Step 4: Check Secondary States
Often multiple states co-occur:
- Cliche (LY1) often comes with Shallow (LY3)
- Meter (LY2) often comes with Sound (LY5)
- Register (LY4) often comes with Structure (LY6)
Step 5: Prioritized Intervention
Fix the most damaging issue first:
- Meter (LY2) before Sound (LY5)—if it's unsingable, sound texture won't help
- Cliche (LY1) before Shallow (LY3)—fresh words enable depth
- Structure (LY6) before Register (LY4)—need sections before matching tone
---
Anti-Patterns
1. Over-Revision
Pattern: Fixing one problem introduces new problems. Every change cascades.
Signs:
- Draft 5 is worse than draft 2
- "Fixing" rhyme breaks meaning
- Original spark is lost
Why It Fails: Lyrics are an interconnected system. Point fixes ignore system effects.
Fix: Make one change, then re-read holistically. Don't chain fixes without checking the whole.
---
2. False Sophistication
Pattern: Chasing complexity for its own sake. Obscurity mistaken for depth.
Signs:
- Lyrics need explanation
- Dense metaphor without clear thread
- Audience confusion, not intrigue
Why It Fails: Obscure isn't deep. Listeners need access point.
Fix: Every obscure element needs a clear anchor. Complexity serves clarity, not replaces it.
---
3. Genre Cosplay
Pattern: Adopting genre vocabulary without understanding function.
Signs:
- Country lyrics with trucks but no storytelling
- Hip-hop wordplay without flow
- Indie obscurity without emotional core
Why It Fails: Genre vocabulary is shortcut to function. Without function, it's costume.
Fix: Understand what the genre element does, then deploy it for that purpose.
---
4. First-Draft Attachment
Pattern: Refusing to revise lines that came easily.
Signs:
- "But that's what I wrote first"
- Defending weak lines emotionally
- Unable to kill darlings
Why It Fails: First drafts capture intuition but include noise. Signal needs extraction.
Fix: Save first draft separately. Revise as if someone else wrote it.
---
Integration
Inbound (when to use this skill)
- After generating AI lyrics (Suno, etc.)
- When human-written lyrics feel "off"
- Before finalizing any lyrics for production
Outbound (what to do after)
- LY1 (Cliche) → Apply Cliche Transcendence framework
- LY2 (Meter) → Use meter-reference.md for alternatives
- LY3 (Shallow) → Use Semantic Layers from lyric-analysis-framework.md
- LY4 (Register) → Check lyricist-dna-framework.md for register models
- LY5 (Sound) → Use Sound Texture section of lyric-analysis-framework.md
- LY6 (Structure) → Use Structural Architecture section
Complementary
- prose-style - for sentence-level craft (overlaps with sound/clarity)
- cliche-transcendence - for escaping default patterns
- naming - for sound symbolism and word selection
---
AI Lyric Generation Failure Modes
AI-generated lyrics (Suno, etc.) fail in predictable ways due to how LLMs work:
The Degradation Pattern
1. Lines 1-2: Model has freedom, may find something interesting 2. Lines 3+: Rhyme constraint activates, model optimizes for completion over originality 3. By verse 2: Fully in statistical default mode—stock phrases, lazy rhyme clusters
Why this happens:
Two forces compound:
1. Statistical median pull: LLMs generate the most probable next token. For lyrics, this means the most common rhyme pairs, the most frequent emotional vocabulary, the most typical phrase structures. Probability optimization produces mediocrity.
2. Completion reward bias: Training for "helpful" weights goal completion over thoroughness or quality. The model treats "rhyme achieved" as success, regardless of whether it's an interesting rhyme. Structure complete > structure meaningful.
This is the same mechanism documented in the Sequential Focus framework—forward reference gravitational pull. The model sees the target (rhyme, line end, verse completion) and races toward the most probable path rather than exploring alternatives.
Observable Symptoms
- Opening verse shows craft, later verses collapse into cliche
- Rhyme words cluster in predictable groups (fire/desire/tired appears together)
- Stock phrases concentrate in verses 2+
- Bridge is often weakest section (furthest from opening, most constrained)
Countermeasures
Pre-anchor end words: Write the rhyme words yourself with intentional non-cluster choices. Force the model to land on words without lazy partners. If you avoid "fire," you break the fire/desire/tired cluster.
Seed with concrete imagery: Opening lines set the vocabulary space. Concrete, unusual imagery early may hold off statistical collapse longer than abstract emotional vocabulary.
Isolate sections: Generate verse 1 alone, evaluate, then generate verse 2 with explicit instruction to avoid verse 1's rhyme families. Treat each section as a separate generation task.
Blacklist clusters: Explicitly forbid the lazy rhyme clusters in prompts:
- "Do not use: fire, desire, tired, higher, liar"
- "Do not use: inside, died, alive, hide, tried"
- "Do not use: heart, apart, start, part"
Provide slant rhyme alternatives: Give the model escape routes. "Instead of perfect rhymes, use slant rhymes like home/come, love/move."
The Isolation Principle
The same principle that makes Sequential Focus work for agent tasks applies to lyric generation: hide the end goal, make each phase complete.
Don't prompt "write a complete song about X." Instead: 1. Generate hook/title concepts (no full lyrics) 2. Generate verse 1 alone (no chorus yet) 3. Generate chorus treating verse 1 as context 4. Generate verse 2 with explicit differentiation requirement 5. Generate bridge with perspective shift requirement
Each step is the complete mission. The model can't race to "song complete" if it doesn't know a song is the goal.
---
What This Skill Does NOT Do
- Does not write lyrics for you (diagnostic only)
- Does not evaluate musical elements (use Musical DNA for that)
- Does not replace human judgment about artistic intent
- Does not guarantee commercial success (craft ≠ commerce)
---
Scripts (Pre-Filters, Not Diagnostics)
These scripts surface candidates for LLM/human interpretation. They do NOT make diagnostic judgments. Use their output as input to your analysis, not as the analysis itself.
Why pre-filters?
- Word lists can't catch stock phrases ("screaming inside")
- Phoneme matching can't judge rhyme quality (gerund-gerund is lazy)
- Syllable counting works, but variance meaning requires interpretation
cliche-check.ts
Surfaces Tier 1/2 terms from lyric-dominance-rules.json. You must evaluate in context.
Usage: bun run scripts/cliche-check.ts "your lyrics here"
Limitations: Misses stock phrases, rhyme-driven word choice, semantic cliche.
meter-check.ts
Reports syllable counts, stress patterns, and variance. You must judge if variance is intentional.
Usage: bun run scripts/meter-check.ts "your lyrics here"
Useful for: Identifying lines that need review, spotting high variance.
rhyme-check.ts
Maps rhyme scheme and identifies rhyme types. You must evaluate rhyme quality.
Usage: bun run scripts/rhyme-check.ts "your lyrics here"
Limitations: Marks gerund-gerund (-ing/-ing) as rhymes. Marks same-semantic-field pairs as rhymes. Doesn't detect rhyme-forced word choice.
---
Quick Reference
| State | Core Problem | First Question | Primary Fix |
|---|---|---|---|
| LY1 | Predictable | Are rhymes first-associations? | Replace cliche terms |
| LY2 | Unsingable | Do stresses match beats? | Count syllables, swap words |
| LY3 | Empty | Is there subtext? | Add concrete imagery |
| LY4 | Mismatched | Does tone match music? | Align vocabulary to intensity |
| LY5 | Harsh | How do words feel in mouth? | Map sounds to emotions |
| LY6 | Aimless | Where's the hook? | Define and place hook |
{
"$schema": "./cmudict-simplified.schema.json",
"version": "1.0",
"source": "Derived from CMU Pronouncing Dictionary - subset of common lyric words",
"stressKey": {
"0": "no stress",
"1": "primary stress",
"2": "secondary stress"
},
"phoneKey": {
"vowels": ["AA", "AE", "AH", "AO", "AW", "AY", "EH", "ER", "EY", "IH", "IY", "OW", "OY", "UH", "UW"],
"consonants": ["B", "CH", "D", "DH", "F", "G", "HH", "JH", "K", "L", "M", "N", "NG", "P", "R", "S", "SH", "T", "TH", "V", "W", "Y", "Z", "ZH"]
},
"words": {
"a": { "phones": ["AH0"], "syllables": 1, "stress": [0] },
"about": { "phones": ["AH0", "B", "AW1", "T"], "syllables": 2, "stress": [0, 1] },
"above": { "phones": ["AH0", "B", "AH1", "V"], "syllables": 2, "stress": [0, 1] },
"again": { "phones": ["AH0", "G", "EH1", "N"], "syllables": 2, "stress": [0, 1] },
"all": { "phones": ["AO1", "L"], "syllables": 1, "stress": [1] },
"alone": { "phones": ["AH0", "L", "OW1", "N"], "syllables": 2, "stress": [0, 1] },
"always": { "phones": ["AO1", "L", "W", "EY0", "Z"], "syllables": 2, "stress": [1, 0] },
"am": { "phones": ["AE1", "M"], "syllables": 1, "stress": [1] },
"and": { "phones": ["AE1", "N", "D"], "syllables": 1, "stress": [1] },
"another": { "phones": ["AH0", "N", "AH1", "DH", "ER0"], "syllables": 3, "stress": [0, 1, 0] },
"are": { "phones": ["AA1", "R"], "syllables": 1, "stress": [1] },
"around": { "phones": ["ER0", "AW1", "N", "D"], "syllables": 2, "stress": [0, 1] },
"away": { "phones": ["AH0", "W", "EY1"], "syllables": 2, "stress": [0, 1] },
"baby": { "phones": ["B", "EY1", "B", "IY0"], "syllables": 2, "stress": [1, 0] },
"back": { "phones": ["B", "AE1", "K"], "syllables": 1, "stress": [1] },
"be": { "phones": ["B", "IY1"], "syllables": 1, "stress": [1] },
"beautiful": { "phones": ["B", "Y", "UW1", "T", "AH0", "F", "AH0", "L"], "syllables": 3, "stress": [1, 0, 0] },
"because": { "phones": ["B", "IH0", "K", "AO1", "Z"], "syllables": 2, "stress": [0, 1] },
"been": { "phones": ["B", "IH1", "N"], "syllables": 1, "stress": [1] },
"before": { "phones": ["B", "IH0", "F", "AO1", "R"], "syllables": 2, "stress": [0, 1] },
"believe": { "phones": ["B", "IH0", "L", "IY1", "V"], "syllables": 2, "stress": [0, 1] },
"blue": { "phones": ["B", "L", "UW1"], "syllables": 1, "stress": [1] },
"break": { "phones": ["B", "R", "EY1", "K"], "syllables": 1, "stress": [1] },
"bring": { "phones": ["B", "R", "IH1", "NG"], "syllables": 1, "stress": [1] },
"broken": { "phones": ["B", "R", "OW1", "K", "AH0", "N"], "syllables": 2, "stress": [1, 0] },
"burn": { "phones": ["B", "ER1", "N"], "syllables": 1, "stress": [1] },
"but": { "phones": ["B", "AH1", "T"], "syllables": 1, "stress": [1] },
"by": { "phones": ["B", "AY1"], "syllables": 1, "stress": [1] },
"call": { "phones": ["K", "AO1", "L"], "syllables": 1, "stress": [1] },
"came": { "phones": ["K", "EY1", "M"], "syllables": 1, "stress": [1] },
"can": { "phones": ["K", "AE1", "N"], "syllables": 1, "stress": [1] },
"carry": { "phones": ["K", "AE1", "R", "IY0"], "syllables": 2, "stress": [1, 0] },
"change": { "phones": ["CH", "EY1", "N", "JH"], "syllables": 1, "stress": [1] },
"close": { "phones": ["K", "L", "OW1", "S"], "syllables": 1, "stress": [1] },
"cold": { "phones": ["K", "OW1", "L", "D"], "syllables": 1, "stress": [1] },
"come": { "phones": ["K", "AH1", "M"], "syllables": 1, "stress": [1] },
"could": { "phones": ["K", "UH1", "D"], "syllables": 1, "stress": [1] },
"cry": { "phones": ["K", "R", "AY1"], "syllables": 1, "stress": [1] },
"dark": { "phones": ["D", "AA1", "R", "K"], "syllables": 1, "stress": [1] },
"day": { "phones": ["D", "EY1"], "syllables": 1, "stress": [1] },
"desire": { "phones": ["D", "IH0", "Z", "AY1", "ER0"], "syllables": 3, "stress": [0, 1, 0] },
"do": { "phones": ["D", "UW1"], "syllables": 1, "stress": [1] },
"don't": { "phones": ["D", "OW1", "N", "T"], "syllables": 1, "stress": [1] },
"down": { "phones": ["D", "AW1", "N"], "syllables": 1, "stress": [1] },
"dream": { "phones": ["D", "R", "IY1", "M"], "syllables": 1, "stress": [1] },
"end": { "phones": ["EH1", "N", "D"], "syllables": 1, "stress": [1] },
"enough": { "phones": ["IH0", "N", "AH1", "F"], "syllables": 2, "stress": [0, 1] },
"even": { "phones": ["IY1", "V", "AH0", "N"], "syllables": 2, "stress": [1, 0] },
"ever": { "phones": ["EH1", "V", "ER0"], "syllables": 2, "stress": [1, 0] },
"every": { "phones": ["EH1", "V", "R", "IY0"], "syllables": 2, "stress": [1, 0] },
"eyes": { "phones": ["AY1", "Z"], "syllables": 1, "stress": [1] },
"face": { "phones": ["F", "EY1", "S"], "syllables": 1, "stress": [1] },
"fall": { "phones": ["F", "AO1", "L"], "syllables": 1, "stress": [1] },
"far": { "phones": ["F", "AA1", "R"], "syllables": 1, "stress": [1] },
"feel": { "phones": ["F", "IY1", "L"], "syllables": 1, "stress": [1] },
"find": { "phones": ["F", "AY1", "N", "D"], "syllables": 1, "stress": [1] },
"fire": { "phones": ["F", "AY1", "ER0"], "syllables": 2, "stress": [1, 0] },
"fly": { "phones": ["F", "L", "AY1"], "syllables": 1, "stress": [1] },
"for": { "phones": ["F", "AO1", "R"], "syllables": 1, "stress": [1] },
"forever": { "phones": ["F", "ER0", "EH1", "V", "ER0"], "syllables": 3, "stress": [0, 1, 0] },
"free": { "phones": ["F", "R", "IY1"], "syllables": 1, "stress": [1] },
"from": { "phones": ["F", "R", "AH1", "M"], "syllables": 1, "stress": [1] },
"give": { "phones": ["G", "IH1", "V"], "syllables": 1, "stress": [1] },
"go": { "phones": ["G", "OW1"], "syllables": 1, "stress": [1] },
"gone": { "phones": ["G", "AO1", "N"], "syllables": 1, "stress": [1] },
"good": { "phones": ["G", "UH1", "D"], "syllables": 1, "stress": [1] },
"hand": { "phones": ["HH", "AE1", "N", "D"], "syllables": 1, "stress": [1] },
"have": { "phones": ["HH", "AE1", "V"], "syllables": 1, "stress": [1] },
"he": { "phones": ["HH", "IY1"], "syllables": 1, "stress": [1] },
"hear": { "phones": ["HH", "IY1", "R"], "syllables": 1, "stress": [1] },
"heart": { "phones": ["HH", "AA1", "R", "T"], "syllables": 1, "stress": [1] },
"here": { "phones": ["HH", "IY1", "R"], "syllables": 1, "stress": [1] },
"high": { "phones": ["HH", "AY1"], "syllables": 1, "stress": [1] },
"hold": { "phones": ["HH", "OW1", "L", "D"], "syllables": 1, "stress": [1] },
"home": { "phones": ["HH", "OW1", "M"], "syllables": 1, "stress": [1] },
"hope": { "phones": ["HH", "OW1", "P"], "syllables": 1, "stress": [1] },
"how": { "phones": ["HH", "AW1"], "syllables": 1, "stress": [1] },
"i": { "phones": ["AY1"], "syllables": 1, "stress": [1] },
"if": { "phones": ["IH1", "F"], "syllables": 1, "stress": [1] },
"in": { "phones": ["IH1", "N"], "syllables": 1, "stress": [1] },
"inside": { "phones": ["IH0", "N", "S", "AY1", "D"], "syllables": 2, "stress": [0, 1] },
"into": { "phones": ["IH1", "N", "T", "UW0"], "syllables": 2, "stress": [1, 0] },
"is": { "phones": ["IH1", "Z"], "syllables": 1, "stress": [1] },
"it": { "phones": ["IH1", "T"], "syllables": 1, "stress": [1] },
"just": { "phones": ["JH", "AH1", "S", "T"], "syllables": 1, "stress": [1] },
"keep": { "phones": ["K", "IY1", "P"], "syllables": 1, "stress": [1] },
"know": { "phones": ["N", "OW1"], "syllables": 1, "stress": [1] },
"last": { "phones": ["L", "AE1", "S", "T"], "syllables": 1, "stress": [1] },
"leave": { "phones": ["L", "IY1", "V"], "syllables": 1, "stress": [1] },
"left": { "phones": ["L", "EH1", "F", "T"], "syllables": 1, "stress": [1] },
"let": { "phones": ["L", "EH1", "T"], "syllables": 1, "stress": [1] },
"life": { "phones": ["L", "AY1", "F"], "syllables": 1, "stress": [1] },
"light": { "phones": ["L", "AY1", "T"], "syllables": 1, "stress": [1] },
"like": { "phones": ["L", "AY1", "K"], "syllables": 1, "stress": [1] },
"little": { "phones": ["L", "IH1", "T", "AH0", "L"], "syllables": 2, "stress": [1, 0] },
"live": { "phones": ["L", "IH1", "V"], "syllables": 1, "stress": [1] },
"long": { "phones": ["L", "AO1", "NG"], "syllables": 1, "stress": [1] },
"look": { "phones": ["L", "UH1", "K"], "syllables": 1, "stress": [1] },
"lose": { "phones": ["L", "UW1", "Z"], "syllables": 1, "stress": [1] },
"lost": { "phones": ["L", "AO1", "S", "T"], "syllables": 1, "stress": [1] },
"love": { "phones": ["L", "AH1", "V"], "syllables": 1, "stress": [1] },
"made": { "phones": ["M", "EY1", "D"], "syllables": 1, "stress": [1] },
"make": { "phones": ["M", "EY1", "K"], "syllables": 1, "stress": [1] },
"man": { "phones": ["M", "AE1", "N"], "syllables": 1, "stress": [1] },
"may": { "phones": ["M", "EY1"], "syllables": 1, "stress": [1] },
"maybe": { "phones": ["M", "EY1", "B", "IY0"], "syllables": 2, "stress": [1, 0] },
"me": { "phones": ["M", "IY1"], "syllables": 1, "stress": [1] },
"mind": { "phones": ["M", "AY1", "N", "D"], "syllables": 1, "stress": [1] },
"moon": { "phones": ["M", "UW1", "N"], "syllables": 1, "stress": [1] },
"more": { "phones": ["M", "AO1", "R"], "syllables": 1, "stress": [1] },
"morning": { "phones": ["M", "AO1", "R", "N", "IH0", "NG"], "syllables": 2, "stress": [1, 0] },
"move": { "phones": ["M", "UW1", "V"], "syllables": 1, "stress": [1] },
"much": { "phones": ["M", "AH1", "CH"], "syllables": 1, "stress": [1] },
"my": { "phones": ["M", "AY1"], "syllables": 1, "stress": [1] },
"name": { "phones": ["N", "EY1", "M"], "syllables": 1, "stress": [1] },
"need": { "phones": ["N", "IY1", "D"], "syllables": 1, "stress": [1] },
"never": { "phones": ["N", "EH1", "V", "ER0"], "syllables": 2, "stress": [1, 0] },
"new": { "phones": ["N", "UW1"], "syllables": 1, "stress": [1] },
"night": { "phones": ["N", "AY1", "T"], "syllables": 1, "stress": [1] },
"no": { "phones": ["N", "OW1"], "syllables": 1, "stress": [1] },
"not": { "phones": ["N", "AA1", "T"], "syllables": 1, "stress": [1] },
"nothing": { "phones": ["N", "AH1", "TH", "IH0", "NG"], "syllables": 2, "stress": [1, 0] },
"now": { "phones": ["N", "AW1"], "syllables": 1, "stress": [1] },
"of": { "phones": ["AH1", "V"], "syllables": 1, "stress": [1] },
"oh": { "phones": ["OW1"], "syllables": 1, "stress": [1] },
"old": { "phones": ["OW1", "L", "D"], "syllables": 1, "stress": [1] },
"on": { "phones": ["AA1", "N"], "syllables": 1, "stress": [1] },
"one": { "phones": ["W", "AH1", "N"], "syllables": 1, "stress": [1] },
"only": { "phones": ["OW1", "N", "L", "IY0"], "syllables": 2, "stress": [1, 0] },
"open": { "phones": ["OW1", "P", "AH0", "N"], "syllables": 2, "stress": [1, 0] },
"or": { "phones": ["AO1", "R"], "syllables": 1, "stress": [1] },
"other": { "phones": ["AH1", "DH", "ER0"], "syllables": 2, "stress": [1, 0] },
"our": { "phones": ["AW1", "R"], "syllables": 1, "stress": [1] },
"out": { "phones": ["AW1", "T"], "syllables": 1, "stress": [1] },
"over": { "phones": ["OW1", "V", "ER0"], "syllables": 2, "stress": [1, 0] },
"pain": { "phones": ["P", "EY1", "N"], "syllables": 1, "stress": [1] },
"place": { "phones": ["P", "L", "EY1", "S"], "syllables": 1, "stress": [1] },
"rain": { "phones": ["R", "EY1", "N"], "syllables": 1, "stress": [1] },
"remember": { "phones": ["R", "IH0", "M", "EH1", "M", "B", "ER0"], "syllables": 3, "stress": [0, 1, 0] },
"right": { "phones": ["R", "AY1", "T"], "syllables": 1, "stress": [1] },
"road": { "phones": ["R", "OW1", "D"], "syllables": 1, "stress": [1] },
"run": { "phones": ["R", "AH1", "N"], "syllables": 1, "stress": [1] },
"said": { "phones": ["S", "EH1", "D"], "syllables": 1, "stress": [1] },
"same": { "phones": ["S", "EY1", "M"], "syllables": 1, "stress": [1] },
"say": { "phones": ["S", "EY1"], "syllables": 1, "stress": [1] },
"see": { "phones": ["S", "IY1"], "syllables": 1, "stress": [1] },
"she": { "phones": ["SH", "IY1"], "syllables": 1, "stress": [1] },
"shine": { "phones": ["SH", "AY1", "N"], "syllables": 1, "stress": [1] },
"should": { "phones": ["SH", "UH1", "D"], "syllables": 1, "stress": [1] },
"side": { "phones": ["S", "AY1", "D"], "syllables": 1, "stress": [1] },
"sky": { "phones": ["S", "K", "AY1"], "syllables": 1, "stress": [1] },
"sleep": { "phones": ["S", "L", "IY1", "P"], "syllables": 1, "stress": [1] },
"so": { "phones": ["S", "OW1"], "syllables": 1, "stress": [1] },
"some": { "phones": ["S", "AH1", "M"], "syllables": 1, "stress": [1] },
"something": { "phones": ["S", "AH1", "M", "TH", "IH0", "NG"], "syllables": 2, "stress": [1, 0] },
"sometimes": { "phones": ["S", "AH1", "M", "T", "AY2", "M", "Z"], "syllables": 2, "stress": [1, 2] },
"song": { "phones": ["S", "AO1", "NG"], "syllables": 1, "stress": [1] },
"soon": { "phones": ["S", "UW1", "N"], "syllables": 1, "stress": [1] },
"soul": { "phones": ["S", "OW1", "L"], "syllables": 1, "stress": [1] },
"star": { "phones": ["S", "T", "AA1", "R"], "syllables": 1, "stress": [1] },
"start": { "phones": ["S", "T", "AA1", "R", "T"], "syllables": 1, "stress": [1] },
"stay": { "phones": ["S", "T", "EY1"], "syllables": 1, "stress": [1] },
"still": { "phones": ["S", "T", "IH1", "L"], "syllables": 1, "stress": [1] },
"sun": { "phones": ["S", "AH1", "N"], "syllables": 1, "stress": [1] },
"take": { "phones": ["T", "EY1", "K"], "syllables": 1, "stress": [1] },
"talk": { "phones": ["T", "AO1", "K"], "syllables": 1, "stress": [1] },
"tears": { "phones": ["T", "IH1", "R", "Z"], "syllables": 1, "stress": [1] },
"tell": { "phones": ["T", "EH1", "L"], "syllables": 1, "stress": [1] },
"than": { "phones": ["DH", "AE1", "N"], "syllables": 1, "stress": [1] },
"that": { "phones": ["DH", "AE1", "T"], "syllables": 1, "stress": [1] },
"the": { "phones": ["DH", "AH0"], "syllables": 1, "stress": [0] },
"their": { "phones": ["DH", "EH1", "R"], "syllables": 1, "stress": [1] },
"them": { "phones": ["DH", "EH1", "M"], "syllables": 1, "stress": [1] },
"then": { "phones": ["DH", "EH1", "N"], "syllables": 1, "stress": [1] },
"there": { "phones": ["DH", "EH1", "R"], "syllables": 1, "stress": [1] },
"these": { "phones": ["DH", "IY1", "Z"], "syllables": 1, "stress": [1] },
"they": { "phones": ["DH", "EY1"], "syllables": 1, "stress": [1] },
"thing": { "phones": ["TH", "IH1", "NG"], "syllables": 1, "stress": [1] },
"think": { "phones": ["TH", "IH1", "NG", "K"], "syllables": 1, "stress": [1] },
"this": { "phones": ["DH", "IH1", "S"], "syllables": 1, "stress": [1] },
"through": { "phones": ["TH", "R", "UW1"], "syllables": 1, "stress": [1] },
"time": { "phones": ["T", "AY1", "M"], "syllables": 1, "stress": [1] },
"to": { "phones": ["T", "UW1"], "syllables": 1, "stress": [1] },
"today": { "phones": ["T", "AH0", "D", "EY1"], "syllables": 2, "stress": [0, 1] },
"together": { "phones": ["T", "AH0", "G", "EH1", "DH", "ER0"], "syllables": 3, "stress": [0, 1, 0] },
"tomorrow": { "phones": ["T", "AH0", "M", "AA1", "R", "OW0"], "syllables": 3, "stress": [0, 1, 0] },
"tonight": { "phones": ["T", "AH0", "N", "AY1", "T"], "syllables": 2, "stress": [0, 1] },
"too": { "phones": ["T", "UW1"], "syllables": 1, "stress": [1] },
"touch": { "phones": ["T", "AH1", "CH"], "syllables": 1, "stress": [1] },
"true": { "phones": ["T", "R", "UW1"], "syllables": 1, "stress": [1] },
"try": { "phones": ["T", "R", "AY1"], "syllables": 1, "stress": [1] },
"turn": { "phones": ["T", "ER1", "N"], "syllables": 1, "stress": [1] },
"under": { "phones": ["AH1", "N", "D", "ER0"], "syllables": 2, "stress": [1, 0] },
"understand": { "phones": ["AH2", "N", "D", "ER0", "S", "T", "AE1", "N", "D"], "syllables": 3, "stress": [2, 0, 1] },
"until": { "phones": ["AH0", "N", "T", "IH1", "L"], "syllables": 2, "stress": [0, 1] },
"up": { "phones": ["AH1", "P"], "syllables": 1, "stress": [1] },
"us": { "phones": ["AH1", "S"], "syllables": 1, "stress": [1] },
"wait": { "phones": ["W", "EY1", "T"], "syllables": 1, "stress": [1] },
"walk": { "phones": ["W", "AO1", "K"], "syllables": 1, "stress": [1] },
"want": { "phones": ["W", "AA1", "N", "T"], "syllables": 1, "stress": [1] },
"was": { "phones": ["W", "AA1", "Z"], "syllables": 1, "stress": [1] },
"water": { "phones": ["W", "AO1", "T", "ER0"], "syllables": 2, "stress": [1, 0] },
"way": { "phones": ["W", "EY1"], "syllables": 1, "stress": [1] },
"we": { "phones": ["W", "IY1"], "syllables": 1, "stress": [1] },
"well": { "phones": ["W", "EH1", "L"], "syllables": 1, "stress": [1] },
"were": { "phones": ["W", "ER1"], "syllables": 1, "stress": [1] },
"what": { "phones": ["W", "AH1", "T"], "syllables": 1, "stress": [1] },
"when": { "phones": ["W", "EH1", "N"], "syllables": 1, "stress": [1] },
"where": { "phones": ["W", "EH1", "R"], "syllables": 1, "stress": [1] },
"while": { "phones": ["W", "AY1", "L"], "syllables": 1, "stress": [1] },
"who": { "phones": ["HH", "UW1"], "syllables": 1, "stress": [1] },
"why": { "phones": ["W", "AY1"], "syllables": 1, "stress": [1] },
"will": { "phones": ["W", "IH1", "L"], "syllables": 1, "stress": [1] },
"wind": { "phones": ["W", "IH1", "N", "D"], "syllables": 1, "stress": [1] },
"with": { "phones": ["W", "IH1", "TH"], "syllables": 1, "stress": [1] },
"without": { "phones": ["W", "IH0", "TH", "AW1", "T"], "syllables": 2, "stress": [0, 1] },
"woman": { "phones": ["W", "UH1", "M", "AH0", "N"], "syllables": 2, "stress": [1, 0] },
"won't": { "phones": ["W", "OW1", "N", "T"], "syllables": 1, "stress": [1] },
"world": { "phones": ["W", "ER1", "L", "D"], "syllables": 1, "stress": [1] },
"would": { "phones": ["W", "UH1", "D"], "syllables": 1, "stress": [1] },
"year": { "phones": ["Y", "IH1", "R"], "syllables": 1, "stress": [1] },
"yes": { "phones": ["Y", "EH1", "S"], "syllables": 1, "stress": [1] },
"yet": { "phones": ["Y", "EH1", "T"], "syllables": 1, "stress": [1] },
"you": { "phones": ["Y", "UW1"], "syllables": 1, "stress": [1] },
"young": { "phones": ["Y", "AH1", "NG"], "syllables": 1, "stress": [1] },
"your": { "phones": ["Y", "AO1", "R"], "syllables": 1, "stress": [1] }
}
}
{
"$schema": "./lyric-dominance-rules.schema.json",
"version": "1.1",
"lastUpdated": "2026-01-02",
"source": "Refined after empirical testing with Suno-generated lyrics",
"rules": [
{
"term": "love",
"context": "as noun or primary topic",
"tier": 1,
"dominancePercentage": 98,
"triggeredCliches": ["love/above", "love/dove", "heart/apart", "forever/together"],
"lyricAlternatives": [
"specific action showing affection",
"concrete sensory memory",
"physical sensation without naming emotion",
"what the person does, not what they feel"
],
"notes": "Triggers generic love song vocabulary. Replace with specific showing, not telling. Instead of 'I love you', show what love looks like in this specific situation."
},
{
"term": "heart",
"context": "as metaphor for emotions",
"tier": 1,
"dominancePercentage": 95,
"triggeredCliches": ["heart/apart", "heart/start", "heart/art", "broken heart"],
"lyricAlternatives": [
"chest, ribcage (physical)",
"specific physical sensation (tightness, warmth)",
"action the emotion causes",
"what the body does under emotion"
],
"notes": "Triggers predictable rhyme pairs and abstract emotional vocabulary. Ground in physical sensation instead."
},
{
"term": "baby",
"context": "as term of address",
"tier": 1,
"dominancePercentage": 92,
"triggeredCliches": ["baby/maybe", "baby/crazy", "generic pop address"],
"lyricAlternatives": [
"character name or nickname",
"specific relationship term (stranger, friend, partner)",
"you (direct address without pet name)",
"no address, just action"
],
"notes": "Triggers generic pop/R&B patterns. Specific names or relationship context create distinction."
},
{
"term": "forever",
"context": "as time reference",
"tier": 1,
"dominancePercentage": 90,
"triggeredCliches": ["forever/together", "forever/never", "forever/weather"],
"lyricAlternatives": [
"specific time reference (til the streetlights come on)",
"generational reference (til your grandkids forget)",
"physical duration (til the rust takes the car)",
"concrete ending point"
],
"notes": "Abstract eternal time is generic. Specific, limited time references are more powerful and honest."
},
{
"term": "always",
"context": "as time/commitment reference",
"tier": 1,
"dominancePercentage": 88,
"triggeredCliches": ["always/hallways", "eternal commitment trope"],
"lyricAlternatives": [
"every time [specific action]",
"each morning when [concrete detail]",
"whenever [specific trigger]"
],
"notes": "Like 'forever', abstract always triggers generic patterns. Specificity adds credibility."
},
{
"term": "dream",
"context": "as abstract concept",
"tier": 2,
"dominancePercentage": 78,
"triggeredCliches": ["dream/seem", "dream/stream", "living the dream"],
"lyricAlternatives": [
"specific aspiration (that house on the corner)",
"concrete image from actual dream",
"what success looks like specifically",
"action toward goal, not goal itself"
],
"notes": "Abstract dreams trigger vague imagery. Concrete dreams (specific house, job, moment) have power."
},
{
"term": "tears",
"context": "as emotional indicator",
"tier": 2,
"dominancePercentage": 75,
"triggeredCliches": ["tears/fears", "tears/years", "crying in the rain"],
"lyricAlternatives": [
"physical manifestation (salt on collar)",
"action during grief (calling no one back)",
"what the room looks like during breakdown",
"what hands do when upset"
],
"notes": "Tears are telling, not showing. Physical details of grief are more effective."
},
{
"term": "soul",
"context": "as spiritual/emotional core",
"tier": 2,
"dominancePercentage": 72,
"triggeredCliches": ["soul/whole", "soul/control", "soul/goal"],
"lyricAlternatives": [
"specific interiority (the quiet part of 3am)",
"what happens in silence",
"the thing no one sees",
"physical location of feeling"
],
"notes": "Abstract soul triggers philosophical cliche. Ground in specific internal experience."
},
{
"term": "fire",
"context": "as metaphor for passion",
"tier": 2,
"dominancePercentage": 85,
"triggeredCliches": ["fire/desire", "fire/higher", "burning love"],
"lyricAlternatives": [
"specific heat sensation",
"what passion makes you do",
"the reckless action, not the feeling",
"consequences of intensity"
],
"notes": "Fire/desire is the most predictable rhyme pair in English. Avoid completely or use slant rhyme."
},
{
"term": "night",
"context": "as time setting",
"tier": 2,
"dominancePercentage": 70,
"triggeredCliches": ["night/light", "night/right", "night/sight", "lonely night"],
"lyricAlternatives": [
"specific time (2:47am)",
"what night sounds like (refrigerator hum)",
"night-specific sensory details",
"what you're doing specifically at night"
],
"notes": "Night/light is predictable. Specific night details (sounds, light quality, activities) are better."
},
{
"term": "rain",
"context": "as weather/emotional metaphor",
"tier": 2,
"dominancePercentage": 68,
"triggeredCliches": ["rain/pain", "rain/again", "crying in the rain"],
"lyricAlternatives": [
"specific weather detail (the smell before rain)",
"what rain sounds like on specific surface",
"action during rain (waiting under awning)",
"aftermath of rain (wet grass, puddles)"
],
"notes": "Rain as sadness metaphor is exhausted. Specific rain details or subverting the metaphor work better."
},
{
"term": "eyes",
"context": "as window to soul/beauty",
"tier": 2,
"dominancePercentage": 65,
"triggeredCliches": ["eyes/skies", "eyes/lies", "eyes/realize"],
"lyricAlternatives": [
"what the eyes do (squint, avoid, search)",
"specific eye behavior (the way they drop)",
"what's seen through the eyes",
"eye-adjacent details (crow's feet, mascara)"
],
"notes": "Eyes as beauty marker is generic. Eye behavior or specific details create character."
},
{
"term": "sky",
"context": "as setting/metaphor",
"tier": 3,
"dominancePercentage": 55,
"triggeredCliches": ["sky/high", "sky/fly", "sky/why", "blue sky"],
"lyricAlternatives": [
"specific sky (the gray before snow)",
"what's in the sky (contrails, hawks)",
"time-of-day sky (4pm October light)",
"weather-specific sky"
],
"notes": "Generic sky triggers generic imagery. Specific skies (time, weather, contents) have distinction."
},
{
"term": "sun",
"context": "as positive metaphor",
"tier": 3,
"dominancePercentage": 50,
"triggeredCliches": ["sun/one", "sun/done", "you are my sunshine"],
"lyricAlternatives": [
"specific sun behavior (slanting through blinds)",
"time-of-day sun (late afternoon gold)",
"sun's effect on objects (bleached dashboard)",
"absence of sun (the shadow side)"
],
"notes": "Sun as positive/hope is predictable. Specific sun moments or subversion work better."
},
{
"term": "moon",
"context": "as romantic/night setting",
"tier": 3,
"dominancePercentage": 52,
"triggeredCliches": ["moon/June", "moon/soon", "moon/tune", "by the light of the moon"],
"lyricAlternatives": [
"specific moon phase (waning crescent)",
"moon behavior (setting behind water tower)",
"moon's effect (silver on the windshield)",
"moon absence (new moon, no light)"
],
"notes": "Moon/June is the classic cliche rhyme. Any use requires intentional approach."
},
{
"term": "time",
"context": "as abstract concept",
"tier": 3,
"dominancePercentage": 48,
"triggeredCliches": ["time/rhyme", "time/mine", "time/crime", "passage of time"],
"lyricAlternatives": [
"specific duration (three Januaries)",
"time marker (before the diagnosis)",
"measured time (forty-seven minutes)",
"time in objects (the calendar we never changed)"
],
"notes": "Abstract time is vague. Specific time markers ground the song in reality."
}
],
"safeTerms": {
"technical": [
"slant rhyme",
"internal rhyme",
"concrete imagery",
"sensory detail",
"iambic",
"conversational meter",
"sparse rhyme density"
],
"descriptive": [
"monosyllabic",
"polysyllabic",
"enjambed",
"end-stopped",
"intimate register",
"confessional mode",
"observational distance"
],
"semantic": [
"showing not telling",
"specific over general",
"physical sensation",
"action-based emotion",
"concrete anchor"
]
},
"genreSpecificWarnings": {
"country": {
"highRisk": ["truck", "beer", "dirt road", "Friday night", "mama"],
"note": "These are expected but oversaturated. Use sparingly with specific detail."
},
"pop": {
"highRisk": ["baby", "tonight", "dance", "move"],
"note": "Functional genre words. Avoid stacking multiple in same verse."
},
"hipHop": {
"highRisk": ["real", "hustle", "grind", "game"],
"note": "Authenticity markers become cliche when overused. Earn each use."
},
"indie": {
"highRisk": ["vintage", "analog", "quiet", "small town"],
"note": "Aesthetic markers. One per song maximum."
}
},
"stockPhrases": [
{
"phrase": "screaming inside",
"category": "unexpressed anguish",
"tier": 1,
"alternatives": [
"specific physical manifestation of suppressed emotion",
"what the body does when holding it in",
"action that reveals internal state"
],
"notes": "Every song about hidden pain uses this exact phrase. Show don't tell."
},
{
"phrase": "need you gone",
"category": "direct desire statement",
"tier": 2,
"alternatives": [
"action toward separation",
"what 'gone' would look like",
"the thing you'd do if they left"
],
"notes": "States desire directly. Show what wanting distance looks like."
},
{
"phrase": "won't leave me alone",
"category": "persistence/haunting",
"tier": 1,
"alternatives": [
"specific intrusion moment",
"when the memory surfaces",
"what triggers the return"
],
"notes": "Generic memory complaint. Make the haunting specific."
},
{
"phrase": "thought I died",
"category": "emotional death metaphor",
"tier": 2,
"alternatives": [
"specific physical sensation of shock",
"what stopped working",
"the moment before and after"
],
"notes": "Overused intensity marker. Find a fresh metaphor for devastation."
},
{
"phrase": "look in your eyes",
"category": "connection/perception",
"tier": 2,
"alternatives": [
"what the eyes are doing specifically",
"what you see in them (concrete)",
"eye-adjacent detail"
],
"notes": "Vague romantic gesture. Make the looking specific."
},
{
"phrase": "torn apart",
"category": "destruction/pain",
"tier": 2,
"alternatives": [
"specific breaking point",
"what actually broke",
"physical sensation of splitting"
],
"notes": "Abstract violence metaphor. Ground in physical reality."
},
{
"phrase": "can't let go",
"category": "attachment",
"tier": 2,
"alternatives": [
"what you're holding onto specifically",
"the action of gripping",
"what letting go would require"
],
"notes": "States the problem. Show the holding instead."
},
{
"phrase": "meant to be",
"category": "destiny/fate",
"tier": 1,
"alternatives": [
"specific coincidence or pattern",
"why this feels inevitable",
"the evidence of rightness"
],
"notes": "Lazy fate invocation. Earn the sense of destiny through specifics."
}
],
"lazyRhymePairs": [
{
"words": ["desire", "fire", "tired", "higher", "liar"],
"cluster": "fire-desire",
"tier": 1,
"notes": "These words cluster together constantly. Using any two triggers cliche. If you use 'fire', find a slant rhyme."
},
{
"words": ["inside", "died", "alive", "hide", "lied", "tried"],
"cluster": "interior-death",
"tier": 1,
"notes": "Death-interior cluster. 'Inside' almost always leads to 'died' or 'alive'."
},
{
"words": ["away", "stay", "day", "say", "way"],
"cluster": "motion-time",
"tier": 2,
"notes": "Motion-time cluster. Very common, not always avoidable, but be intentional."
},
{
"words": ["heart", "apart", "start", "part", "art"],
"cluster": "heart-destruction",
"tier": 1,
"notes": "Heart-destruction cluster. 'Heart' almost guarantees 'apart' follows."
},
{
"words": ["alone", "gone", "on", "dawn", "song"],
"cluster": "isolation",
"tier": 2,
"notes": "Isolation cluster. 'Alone' naturally drifts toward 'gone'."
},
{
"words": ["love", "above", "of", "dove"],
"cluster": "love-above",
"tier": 1,
"notes": "The original cliche rhyme. Any of these together triggers immediate recognition."
},
{
"words": ["pain", "rain", "again", "vain", "insane"],
"cluster": "suffering",
"tier": 2,
"notes": "Suffering cluster. 'Pain' reliably finds 'rain' or 'again'."
}
],
"rhymeQualityWarnings": {
"gerundGerund": {
"pattern": "Two -ing words rhyming",
"examples": ["laughing/smiling", "crying/dying", "falling/calling"],
"problem": "Shared suffix, not real sonic relationship. Same semantic field makes it worse.",
"tier": 2
},
"sameSemanticField": {
"pattern": "Rhyming words from same category",
"examples": ["laughing/smiling (both expressions)", "walking/running (both motion)", "thinking/feeling (both cognition)"],
"problem": "No contrast. Rhyme should create unexpected connection, not confirm the obvious.",
"tier": 2
},
"suffixOnly": {
"pattern": "Rhyme only in suffix, not root",
"examples": ["-tion/-tion", "-ly/-ly", "-ness/-ness"],
"problem": "Grammatical rhyme, not sonic. Feels like accident.",
"tier": 3
}
},
"structuralCliches": {
"bridgeEqualsVerse": {
"pattern": "Bridge content identical or near-identical to a verse",
"problem": "Not a bridge at all. Bridge must shift perspective, time, or speaker.",
"fix": "Ask: what hasn't been said yet? Say that in the bridge."
},
"chorusExpansion": {
"pattern": "Chorus adds lines on later repeats without development",
"problem": "More words ≠ more meaning. Padding disguised as development.",
"fix": "Each chorus iteration should recontextualize through what came before, not through added words."
},
"hookWithoutExploration": {
"pattern": "Title/hook phrase repeated but never unpacked",
"problem": "We hear it but never learn what it means to the speaker.",
"fix": "Verses should show us different facets of the hook's meaning."
},
"noArc": {
"pattern": "Speaker's emotional state identical at end and beginning",
"problem": "Nothing happened. Why did we listen?",
"fix": "Something must change: realization, acceptance, escalation, or collapse."
}
}
}
#!/usr/bin/env bun
/**
* cliche-check.ts
*
* Analyzes lyrics for cliche terms using the lyric-dominance-rules.json data.
* Identifies Tier 1 (always avoid) and Tier 2 (avoid by default) terms.
*
* Usage: bun run scripts/cliche-check.ts "your lyrics here"
* bun run scripts/cliche-check.ts --file lyrics.txt
*/
import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
interface DominanceRule {
term: string;
context?: string;
tier: 1 | 2 | 3;
dominancePercentage: number;
triggeredCliches: string[];
lyricAlternatives: string[];
notes: string;
}
interface DominanceRules {
version: string;
rules: DominanceRule[];
safeTerms: {
technical: string[];
descriptive: string[];
semantic: string[];
};
genreSpecificWarnings: Record<string, { highRisk: string[]; note: string }>;
}
interface ClicheMatch {
term: string;
tier: 1 | 2 | 3;
dominancePercentage: number;
context: string;
position: number;
alternatives: string[];
notes: string;
}
function loadRules(): DominanceRules {
const rulesPath = resolve(__dirname, "../data/lyric-dominance-rules.json");
const content = readFileSync(rulesPath, "utf-8");
return JSON.parse(content);
}
function findCliches(text: string, rules: DominanceRules): ClicheMatch[] {
const matches: ClicheMatch[] = [];
const lowerText = text.toLowerCase();
const words = lowerText.split(/\s+/);
for (const rule of rules.rules) {
const termLower = rule.term.toLowerCase();
// Find all occurrences
let position = 0;
let searchStart = 0;
while ((position = lowerText.indexOf(termLower, searchStart)) !== -1) {
// Check word boundaries
const before = position === 0 ? " " : lowerText[position - 1];
const after = position + termLower.length >= lowerText.length
? " "
: lowerText[position + termLower.length];
const isWordBoundary = /[\s.,!?;:'"()[\]{}]/.test(before) &&
/[\s.,!?;:'"()[\]{}]/.test(after);
if (isWordBoundary) {
// Get surrounding context (10 chars each side)
const contextStart = Math.max(0, position - 20);
const contextEnd = Math.min(lowerText.length, position + termLower.length + 20);
const context = "..." + text.slice(contextStart, contextEnd) + "...";
matches.push({
term: rule.term,
tier: rule.tier,
dominancePercentage: rule.dominancePercentage,
context: context.trim(),
position,
alternatives: rule.lyricAlternatives,
notes: rule.notes
});
}
searchStart = position + 1;
}
}
// Sort by tier (most severe first), then by position
return matches.sort((a, b) => {
if (a.tier !== b.tier) return a.tier - b.tier;
return a.position - b.position;
});
}
function formatResults(matches: ClicheMatch[], text: string): string {
const lines: string[] = [];
lines.push("=".repeat(60));
lines.push("LYRIC CLICHE ANALYSIS");
lines.push("=".repeat(60));
lines.push("");
const tier1 = matches.filter(m => m.tier === 1);
const tier2 = matches.filter(m => m.tier === 2);
const tier3 = matches.filter(m => m.tier === 3);
// Summary
lines.push(`SUMMARY: ${matches.length} potential cliche terms found`);
lines.push(` Tier 1 (ALWAYS AVOID): ${tier1.length}`);
lines.push(` Tier 2 (AVOID BY DEFAULT): ${tier2.length}`);
lines.push(` Tier 3 (MODERATE RISK): ${tier3.length}`);
lines.push("");
if (tier1.length > 0) {
lines.push("-".repeat(60));
lines.push("TIER 1 - ALWAYS AVOID (95%+ cliche trigger)");
lines.push("-".repeat(60));
for (const match of tier1) {
lines.push(`\nTerm: "${match.term}" (${match.dominancePercentage}% dominance)`);
lines.push(`Context: ${match.context}`);
lines.push(`Notes: ${match.notes}`);
lines.push("Alternatives:");
for (const alt of match.alternatives.slice(0, 3)) {
lines.push(` - ${alt}`);
}
}
lines.push("");
}
if (tier2.length > 0) {
lines.push("-".repeat(60));
lines.push("TIER 2 - AVOID BY DEFAULT (70-90% cliche trigger)");
lines.push("-".repeat(60));
for (const match of tier2) {
lines.push(`\nTerm: "${match.term}" (${match.dominancePercentage}% dominance)`);
lines.push(`Context: ${match.context}`);
lines.push(`Notes: ${match.notes}`);
lines.push("Alternatives:");
for (const alt of match.alternatives.slice(0, 3)) {
lines.push(` - ${alt}`);
}
}
lines.push("");
}
if (tier3.length > 0) {
lines.push("-".repeat(60));
lines.push("TIER 3 - MODERATE RISK (40-60% cliche trigger)");
lines.push("-".repeat(60));
for (const match of tier3) {
lines.push(`\nTerm: "${match.term}" (${match.dominancePercentage}% dominance)`);
lines.push(`Context: ${match.context}`);
}
lines.push("");
}
if (matches.length === 0) {
lines.push("No high-risk cliche terms detected.");
lines.push("This doesn't mean the lyrics are cliche-free - only that");
lines.push("the most common trigger words are absent.");
}
lines.push("");
lines.push("=".repeat(60));
return lines.join("\n");
}
// Main execution
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("Usage: bun run scripts/cliche-check.ts <lyrics>");
console.log(" bun run scripts/cliche-check.ts --file <path>");
process.exit(1);
}
let text: string;
if (args[0] === "--file" && args[1]) {
text = readFileSync(args[1], "utf-8");
} else {
text = args.join(" ");
}
const rules = loadRules();
const matches = findCliches(text, rules);
console.log(formatResults(matches, text));
#!/usr/bin/env bun
/**
* meter-check.ts
*
* Analyzes lyrics for syllable counts and stress patterns using CMUDict data.
* Identifies potential meter mismatches and suggests alternatives.
*
* Usage: bun run scripts/meter-check.ts "your lyrics here"
* bun run scripts/meter-check.ts --file lyrics.txt
*/
import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
interface WordEntry {
phones: string[];
syllables: number;
stress: number[];
}
interface CMUDict {
words: Record<string, WordEntry>;
phoneKey: {
vowels: string[];
consonants: string[];
};
}
interface LineAnalysis {
text: string;
syllableCount: number;
stressPattern: string;
unknownWords: string[];
wordBreakdown: { word: string; syllables: number; stress: string }[];
}
function loadCMUDict(): CMUDict {
const dictPath = resolve(__dirname, "../data/cmudict-simplified.json");
const content = readFileSync(dictPath, "utf-8");
return JSON.parse(content);
}
function cleanWord(word: string): string {
return word.toLowerCase().replace(/[^a-z']/g, "");
}
function estimateSyllables(word: string): number {
// Fallback syllable counting for unknown words
const vowels = "aeiouy";
let count = 0;
let prevVowel = false;
const cleaned = word.toLowerCase();
for (let i = 0; i < cleaned.length; i++) {
const isVowel = vowels.includes(cleaned[i]);
if (isVowel && !prevVowel) {
count++;
}
prevVowel = isVowel;
}
// Handle silent e
if (cleaned.endsWith("e") && count > 1 && !cleaned.endsWith("le")) {
count--;
}
// Handle -ed endings
if (cleaned.endsWith("ed") && !cleaned.endsWith("ted") && !cleaned.endsWith("ded")) {
count--;
}
return Math.max(1, count);
}
function estimateStress(word: string, syllables: number): number[] {
// Simple heuristic for stress when word not in dictionary
if (syllables === 1) return [1];
if (syllables === 2) return [1, 0]; // Default trochaic
if (syllables === 3) return [1, 0, 0]; // Default dactylic
return [1, ...Array(syllables - 1).fill(0)];
}
function analyzeLine(line: string, dict: CMUDict): LineAnalysis {
const words = line.split(/\s+/).filter(w => w.length > 0);
const wordBreakdown: { word: string; syllables: number; stress: string }[] = [];
const unknownWords: string[] = [];
let totalSyllables = 0;
let stressPattern = "";
for (const rawWord of words) {
const word = cleanWord(rawWord);
if (!word) continue;
const entry = dict.words[word];
if (entry) {
totalSyllables += entry.syllables;
const stressStr = entry.stress.map(s => s === 0 ? "u" : "/").join("");
stressPattern += stressStr + " ";
wordBreakdown.push({
word: rawWord,
syllables: entry.syllables,
stress: stressStr
});
} else {
unknownWords.push(word);
const estimatedSyllables = estimateSyllables(word);
const estimatedStress = estimateStress(word, estimatedSyllables);
totalSyllables += estimatedSyllables;
const stressStr = estimatedStress.map(s => s === 0 ? "u" : "/").join("");
stressPattern += stressStr + "? ";
wordBreakdown.push({
word: rawWord + "?",
syllables: estimatedSyllables,
stress: stressStr + "?"
});
}
}
return {
text: line,
syllableCount: totalSyllables,
stressPattern: stressPattern.trim(),
unknownWords,
wordBreakdown
};
}
function identifyMeter(stressPattern: string): string {
// Clean the pattern
const clean = stressPattern.replace(/[\s?]/g, "");
// Check for common patterns
if (/^(u\/)+u?$/.test(clean)) return "iambic";
if (/^(\/u)+\/?$/.test(clean)) return "trochaic";
if (/^(uu\/)+u?u?$/.test(clean)) return "anapestic";
if (/^(\/uu)+\/?u?$/.test(clean)) return "dactylic";
if (/^(\/\/)+\/?$/.test(clean)) return "spondaic";
return "mixed/free";
}
function formatResults(analyses: LineAnalysis[]): string {
const lines: string[] = [];
lines.push("=".repeat(60));
lines.push("LYRIC METER ANALYSIS");
lines.push("=".repeat(60));
lines.push("");
// Summary statistics
const syllableCounts = analyses.map(a => a.syllableCount);
const avgSyllables = syllableCounts.reduce((a, b) => a + b, 0) / syllableCounts.length;
const minSyllables = Math.min(...syllableCounts);
const maxSyllables = Math.max(...syllableCounts);
const variance = maxSyllables - minSyllables;
lines.push("SUMMARY");
lines.push("-".repeat(40));
lines.push(`Lines analyzed: ${analyses.length}`);
lines.push(`Average syllables per line: ${avgSyllables.toFixed(1)}`);
lines.push(`Range: ${minSyllables} - ${maxSyllables} (variance: ${variance})`);
lines.push("");
// Variance warning
if (variance > 4) {
lines.push("WARNING: High syllable variance between lines.");
lines.push("This may indicate meter inconsistency.");
lines.push("");
}
// Line-by-line analysis
lines.push("LINE-BY-LINE ANALYSIS");
lines.push("-".repeat(40));
for (let i = 0; i < analyses.length; i++) {
const analysis = analyses[i];
const meter = identifyMeter(analysis.stressPattern);
lines.push(`\nLine ${i + 1}: "${analysis.text}"`);
lines.push(` Syllables: ${analysis.syllableCount}`);
lines.push(` Stress: ${analysis.stressPattern}`);
lines.push(` Meter: ${meter}`);
if (analysis.unknownWords.length > 0) {
lines.push(` Unknown words (estimated): ${analysis.unknownWords.join(", ")}`);
}
// Word breakdown
lines.push(` Breakdown:`);
for (const wb of analysis.wordBreakdown) {
lines.push(` "${wb.word}": ${wb.syllables} syl [${wb.stress}]`);
}
}
// Overall meter assessment
const allPatterns = analyses.map(a => identifyMeter(a.stressPattern));
const meterCounts: Record<string, number> = {};
for (const m of allPatterns) {
meterCounts[m] = (meterCounts[m] || 0) + 1;
}
lines.push("");
lines.push("-".repeat(40));
lines.push("METER DISTRIBUTION");
for (const [meter, count] of Object.entries(meterCounts)) {
const pct = ((count / analyses.length) * 100).toFixed(0);
lines.push(` ${meter}: ${count} lines (${pct}%)`);
}
lines.push("");
lines.push("=".repeat(60));
lines.push("");
lines.push("Key: / = stressed, u = unstressed, ? = estimated");
return lines.join("\n");
}
// Main execution
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("Usage: bun run scripts/meter-check.ts <lyrics>");
console.log(" bun run scripts/meter-check.ts --file <path>");
process.exit(1);
}
let text: string;
if (args[0] === "--file" && args[1]) {
text = readFileSync(args[1], "utf-8");
} else {
text = args.join(" ");
}
const dict = loadCMUDict();
// Split into lines and analyze each
const textLines = text.split(/\n/).filter(line => line.trim().length > 0);
const analyses = textLines.map(line => analyzeLine(line.trim(), dict));
console.log(formatResults(analyses));
#!/usr/bin/env bun
/**
* rhyme-check.ts
*
* Analyzes lyrics for rhyme scheme and rhyme types.
* Identifies perfect rhymes, slant rhymes, and internal rhymes.
*
* Usage: bun run scripts/rhyme-check.ts "your lyrics here"
* bun run scripts/rhyme-check.ts --file lyrics.txt
*/
import { readFileSync } from "fs";
import { resolve, dirname } from "path";
import { fileURLToPath } from "url";
const __dirname = dirname(fileURLToPath(import.meta.url));
interface WordEntry {
phones: string[];
syllables: number;
stress: number[];
}
interface CMUDict {
words: Record<string, WordEntry>;
phoneKey: {
vowels: string[];
consonants: string[];
};
}
type RhymeType = "perfect" | "slant" | "eye" | "none";
interface RhymePair {
word1: string;
word2: string;
line1: number;
line2: number;
type: RhymeType;
phones1?: string[];
phones2?: string[];
}
function loadCMUDict(): CMUDict {
const dictPath = resolve(__dirname, "../data/cmudict-simplified.json");
const content = readFileSync(dictPath, "utf-8");
return JSON.parse(content);
}
function cleanWord(word: string): string {
return word.toLowerCase().replace(/[^a-z']/g, "");
}
function getLastWord(line: string): string {
const words = line.trim().split(/\s+/);
const lastWord = words[words.length - 1];
return cleanWord(lastWord);
}
function getEndingPhones(phones: string[]): string[] {
// Get phones from last stressed vowel onward
const vowels = ["AA", "AE", "AH", "AO", "AW", "AY", "EH", "ER", "EY", "IH", "IY", "OW", "OY", "UH", "UW"];
let lastStressIndex = -1;
for (let i = phones.length - 1; i >= 0; i--) {
const phone = phones[i].replace(/[012]$/, "");
if (vowels.includes(phone) && phones[i].match(/[12]$/)) {
lastStressIndex = i;
break;
}
}
if (lastStressIndex === -1) {
// No stressed vowel found, use last vowel
for (let i = phones.length - 1; i >= 0; i--) {
const phone = phones[i].replace(/[012]$/, "");
if (vowels.includes(phone)) {
lastStressIndex = i;
break;
}
}
}
if (lastStressIndex === -1) return phones;
return phones.slice(lastStressIndex);
}
function stripStress(phones: string[]): string[] {
return phones.map(p => p.replace(/[012]$/, ""));
}
function classifyRhyme(word1: string, word2: string, dict: CMUDict): RhymeType {
if (word1 === word2) return "none"; // Identical words don't count
const entry1 = dict.words[word1];
const entry2 = dict.words[word2];
// Check eye rhyme (same ending spelling, may not have same sound)
if (word1.slice(-2) === word2.slice(-2) && word1.length > 2 && word2.length > 2) {
// Could be eye rhyme if not perfect
}
if (!entry1 || !entry2) {
// Fallback: simple ending comparison
if (word1.slice(-3) === word2.slice(-3)) return "perfect";
if (word1.slice(-2) === word2.slice(-2)) return "slant";
return "none";
}
const ending1 = getEndingPhones(entry1.phones);
const ending2 = getEndingPhones(entry2.phones);
const stripped1 = stripStress(ending1);
const stripped2 = stripStress(ending2);
// Perfect rhyme: identical ending sounds
if (stripped1.join(" ") === stripped2.join(" ")) {
return "perfect";
}
// Slant rhyme: similar but not identical
// Check if vowels match but consonants differ, or vice versa
const vowels = ["AA", "AE", "AH", "AO", "AW", "AY", "EH", "ER", "EY", "IH", "IY", "OW", "OY", "UH", "UW"];
const vowel1 = stripped1.find(p => vowels.includes(p));
const vowel2 = stripped2.find(p => vowels.includes(p));
if (vowel1 && vowel2) {
// Same vowel = potential slant
if (vowel1 === vowel2) return "slant";
// Similar vowels
const similarVowels: string[][] = [
["IY", "IH", "EY"],
["EH", "AE"],
["AA", "AH", "AO"],
["OW", "UW", "UH"],
["AY", "OY"],
["AW", "OW"]
];
for (const group of similarVowels) {
if (group.includes(vowel1) && group.includes(vowel2)) {
return "slant";
}
}
}
// Check eye rhyme
if (word1.slice(-3) === word2.slice(-3)) {
return "eye";
}
return "none";
}
function analyzeRhymeScheme(lines: string[], dict: CMUDict): {
scheme: string;
pairs: RhymePair[];
density: number;
} {
const endWords = lines.map(line => getLastWord(line));
const pairs: RhymePair[] = [];
const schemeLetters: string[] = [];
const wordToLetter: Map<string, string> = new Map();
let currentLetter = "A";
for (let i = 0; i < endWords.length; i++) {
const word = endWords[i];
let foundRhyme = false;
// Check against previous lines
for (let j = 0; j < i; j++) {
const prevWord = endWords[j];
const rhymeType = classifyRhyme(word, prevWord, dict);
if (rhymeType === "perfect" || rhymeType === "slant") {
pairs.push({
word1: prevWord,
word2: word,
line1: j + 1,
line2: i + 1,
type: rhymeType,
phones1: dict.words[prevWord]?.phones,
phones2: dict.words[word]?.phones
});
// Use same letter as rhyming line
if (schemeLetters[j] && !foundRhyme) {
schemeLetters[i] = schemeLetters[j];
foundRhyme = true;
}
}
}
if (!foundRhyme) {
schemeLetters[i] = currentLetter;
currentLetter = String.fromCharCode(currentLetter.charCodeAt(0) + 1);
if (currentLetter > "Z") currentLetter = "a";
}
}
const scheme = schemeLetters.join("");
const density = pairs.length / Math.max(1, lines.length - 1);
return { scheme, pairs, density };
}
function findInternalRhymes(lines: string[], dict: CMUDict): RhymePair[] {
const internalPairs: RhymePair[] = [];
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
const words = lines[lineNum].split(/\s+/).map(w => cleanWord(w)).filter(w => w.length > 2);
for (let i = 0; i < words.length; i++) {
for (let j = i + 1; j < words.length; j++) {
const rhymeType = classifyRhyme(words[i], words[j], dict);
if (rhymeType === "perfect" || rhymeType === "slant") {
internalPairs.push({
word1: words[i],
word2: words[j],
line1: lineNum + 1,
line2: lineNum + 1,
type: rhymeType
});
}
}
}
}
return internalPairs;
}
interface RhymeQualityWarning {
pair: [string, string];
warning: string;
severity: "high" | "medium" | "low";
}
function checkRhymeQuality(pairs: RhymePair[]): RhymeQualityWarning[] {
const warnings: RhymeQualityWarning[] = [];
// Lazy rhyme clusters from empirical testing
const lazyClusters: { words: string[]; name: string }[] = [
{ words: ["desire", "fire", "tired", "higher", "liar"], name: "fire-desire cluster" },
{ words: ["inside", "died", "alive", "hide", "lied", "tried"], name: "interior-death cluster" },
{ words: ["heart", "apart", "start", "part", "art"], name: "heart-destruction cluster" },
{ words: ["alone", "gone", "on", "dawn", "song"], name: "isolation cluster" },
{ words: ["love", "above", "of", "dove"], name: "love-above cluster" },
{ words: ["pain", "rain", "again", "vain", "insane"], name: "suffering cluster" },
];
// Semantic field categories for same-field detection
const semanticFields: Record<string, string[]> = {
expressions: ["laughing", "smiling", "crying", "frowning", "grinning", "weeping"],
motion: ["walking", "running", "falling", "flying", "moving", "standing"],
cognition: ["thinking", "feeling", "knowing", "dreaming", "hoping", "wishing"],
emotion: ["loving", "hating", "fearing", "wanting", "needing", "craving"],
};
for (const pair of pairs) {
const w1 = pair.word1.toLowerCase();
const w2 = pair.word2.toLowerCase();
// Check gerund-gerund (-ing/-ing)
if (w1.endsWith("ing") && w2.endsWith("ing")) {
warnings.push({
pair: [w1, w2],
warning: "Gerund-gerund rhyme: shared -ing suffix, not real sonic relationship",
severity: "medium"
});
}
// Check same semantic field
for (const [field, words] of Object.entries(semanticFields)) {
if (words.includes(w1) && words.includes(w2)) {
warnings.push({
pair: [w1, w2],
warning: `Same semantic field (${field}): no contrast, confirms the obvious`,
severity: "medium"
});
break;
}
}
// Check lazy clusters
for (const cluster of lazyClusters) {
if (cluster.words.includes(w1) && cluster.words.includes(w2)) {
warnings.push({
pair: [w1, w2],
warning: `${cluster.name}: overused pairing`,
severity: "high"
});
break;
}
}
// Check suffix-only rhymes (-tion/-tion, -ly/-ly, -ness/-ness)
const suffixes = ["tion", "sion", "ness", "ment", "able", "ible"];
for (const suffix of suffixes) {
if (w1.endsWith(suffix) && w2.endsWith(suffix) && w1 !== w2) {
// Only warn if the root is different
const root1 = w1.slice(0, -suffix.length);
const root2 = w2.slice(0, -suffix.length);
if (root1 !== root2) {
warnings.push({
pair: [w1, w2],
warning: `Suffix-only rhyme (-${suffix}): grammatical, not sonic`,
severity: "low"
});
break;
}
}
}
}
return warnings;
}
function formatResults(
lines: string[],
analysis: { scheme: string; pairs: RhymePair[]; density: number },
internalRhymes: RhymePair[],
qualityWarnings: RhymeQualityWarning[]
): string {
const output: string[] = [];
output.push("=".repeat(60));
output.push("LYRIC RHYME ANALYSIS");
output.push("=".repeat(60));
output.push("");
// Summary
output.push("SUMMARY");
output.push("-".repeat(40));
output.push(`Lines analyzed: ${lines.length}`);
output.push(`Rhyme scheme: ${analysis.scheme}`);
output.push(`End rhyme pairs found: ${analysis.pairs.length}`);
output.push(`Internal rhyme pairs found: ${internalRhymes.length}`);
output.push(`Rhyme density: ${(analysis.density * 100).toFixed(0)}%`);
output.push("");
// Scheme with lines
output.push("RHYME SCHEME");
output.push("-".repeat(40));
for (let i = 0; i < lines.length; i++) {
const letter = analysis.scheme[i] || "?";
const truncatedLine = lines[i].length > 45
? lines[i].slice(0, 45) + "..."
: lines[i];
output.push(`${letter}: ${truncatedLine}`);
}
output.push("");
// End rhyme pairs
if (analysis.pairs.length > 0) {
output.push("END RHYME PAIRS");
output.push("-".repeat(40));
const perfectPairs = analysis.pairs.filter(p => p.type === "perfect");
const slantPairs = analysis.pairs.filter(p => p.type === "slant");
if (perfectPairs.length > 0) {
output.push("\nPerfect Rhymes:");
for (const pair of perfectPairs) {
output.push(` "${pair.word1}" (line ${pair.line1}) / "${pair.word2}" (line ${pair.line2})`);
}
}
if (slantPairs.length > 0) {
output.push("\nSlant Rhymes:");
for (const pair of slantPairs) {
output.push(` "${pair.word1}" (line ${pair.line1}) / "${pair.word2}" (line ${pair.line2})`);
}
}
output.push("");
}
// Internal rhymes
if (internalRhymes.length > 0) {
output.push("INTERNAL RHYMES");
output.push("-".repeat(40));
for (const pair of internalRhymes) {
output.push(` Line ${pair.line1}: "${pair.word1}" / "${pair.word2}" (${pair.type})`);
}
output.push("");
}
// Assessment
output.push("ASSESSMENT");
output.push("-".repeat(40));
if (analysis.density === 0) {
output.push("No end rhymes detected - free verse or prose-like.");
} else if (analysis.density < 0.3) {
output.push("Sparse rhyming - modern/indie style.");
} else if (analysis.density < 0.6) {
output.push("Moderate rhyming - balanced approach.");
} else {
output.push("Dense rhyming - traditional/structured form.");
}
// Quality warnings
if (qualityWarnings.length > 0) {
output.push("");
output.push("RHYME QUALITY WARNINGS");
output.push("-".repeat(40));
const highSeverity = qualityWarnings.filter(w => w.severity === "high");
const mediumSeverity = qualityWarnings.filter(w => w.severity === "medium");
const lowSeverity = qualityWarnings.filter(w => w.severity === "low");
if (highSeverity.length > 0) {
output.push("\n[HIGH] Lazy rhyme clusters:");
for (const w of highSeverity) {
output.push(` "${w.pair[0]}" / "${w.pair[1]}": ${w.warning}`);
}
}
if (mediumSeverity.length > 0) {
output.push("\n[MEDIUM] Weak rhyme patterns:");
for (const w of mediumSeverity) {
output.push(` "${w.pair[0]}" / "${w.pair[1]}": ${w.warning}`);
}
}
if (lowSeverity.length > 0) {
output.push("\n[LOW] Suffix-dependent rhymes:");
for (const w of lowSeverity) {
output.push(` "${w.pair[0]}" / "${w.pair[1]}": ${w.warning}`);
}
}
}
// Check for cliche rhyme pairs
const clichePairs = [
["love", "above"], ["love", "dove"], ["heart", "apart"], ["heart", "start"],
["fire", "desire"], ["night", "light"], ["moon", "June"], ["eyes", "skies"],
["forever", "together"], ["rain", "pain"], ["dream", "seem"]
];
const foundCliches: string[] = [];
for (const pair of analysis.pairs) {
for (const [a, b] of clichePairs) {
if ((pair.word1 === a && pair.word2 === b) ||
(pair.word1 === b && pair.word2 === a)) {
foundCliches.push(`${a}/${b}`);
}
}
}
if (foundCliches.length > 0) {
output.push("");
output.push("WARNING: Cliche rhyme pairs detected:");
for (const cliche of foundCliches) {
output.push(` - ${cliche}`);
}
output.push("Consider slant rhymes or restructuring.");
}
output.push("");
output.push("=".repeat(60));
return output.join("\n");
}
// Main execution
const args = process.argv.slice(2);
if (args.length === 0) {
console.log("Usage: bun run scripts/rhyme-check.ts <lyrics>");
console.log(" bun run scripts/rhyme-check.ts --file <path>");
process.exit(1);
}
let text: string;
if (args[0] === "--file" && args[1]) {
text = readFileSync(args[1], "utf-8");
} else {
text = args.join(" ");
}
const dict = loadCMUDict();
// Split into lines
const lines = text.split(/\n/).filter(line => line.trim().length > 0);
if (lines.length < 2) {
console.log("Need at least 2 lines to analyze rhyme scheme.");
console.log("For single-line analysis, internal rhymes will be checked.");
}
const analysis = analyzeRhymeScheme(lines, dict);
const internalRhymes = findInternalRhymes(lines, dict);
const allPairs = [...analysis.pairs, ...internalRhymes];
const qualityWarnings = checkRhymeQuality(allPairs);
console.log(formatResults(lines, analysis, internalRhymes, qualityWarnings));