Skip to content
All writing

Count the lines, do not trust them

Ask a model for seven characters a line and a rhyme at the end, and it returns something that looks right. We changed it to count instead: syllables measured line by line, rhymes filed by the thirteen Mandarin rhyme groups, and only the lines that missed get rewritten. Here is the loop, and the pinyin bug we hit on the way.

3 min readFiled underEngineeringProduct

A lyricist never says "please try to rhyme". They have a melody, and the melody hands them each line's length directly — six characters here, nine in the next line, four in the chorus. That is not a stylistic preference. It is a hard constraint.

Language models are very good at producing something that looks constrained. Ask for seven characters a line and you get a mix of six, seven and eight that reads perfectly well. Ask for a rhyme and you get 光 and 亮 — which do not rhyme in Mandarin at all. They are in different rhyme groups entirely.

The problem is not that the model is not good enough. The problem is that nobody counted.

Count first, discuss later

In 詞光 LyricLab this is three steps, and the middle one is the point:

  1. The model writes a pass against the topic and the line pattern.
  2. Code measures every line: character count, and which rhyme group the ending falls in.
  3. The lines that missed — only those lines — go back with a description of the miss.

Step two involves no model at all. Length is length. The rhyme is a table lookup. So what comes out is not "the model says it complied", it is "we counted". The status line in the demo on the home page is not decorative either; it reports where that loop actually is.

The model proposes lines. The code is what says they do not count.

The only design rule in the whole feature

Rhyme is not a suffix match

The rhyme check is the easiest part to get wrong. The intuitive approach is to compare the pinyin ending of the last character, and it fails badly.

Mandarin has a traditional system for this: 十三轍, thirteen rhyme groups that a working lyricist already carries in their head. Same group rhymes, different group does not, and it has nothing to do with how similar the spellings look:

PairVerdictWhy
昏 (hun) / 群 (qun)RhymesBoth in 人辰
昏 (hun) / 孤 (gu)Does not人辰 against 姑蘇
光 (guang) / 亮 (liang)Does not江陽 against 遙條

So lib/rhyme.ts takes the last Han character of the line, gets its final from pinyin-pro, and maps that final onto one of the thirteen. Tone is ignored on purpose, which is what singing does anyway.

The bug we hit

pinyin-pro reports ü correctly after n, l, j, q and x. It does not after y.

魚 comes back as u and 月 as ue. Taken at face value, 魚 gets filed under 姑蘇 and rhymes with 孤 — wrong, and quietly wrong: no exception, no warning, just a few lines in a song waved through for no visible reason.

That class of bug is the cost of verifying in code. Take the judgement back from the model and you own whether the judgement is right. The difference is that when code is wrong you can pin it with a test. When a model is wrong you can only ask it again.

Near rhyme is a setting, not a shrug

In practice, strict same-group matching is too tight to write against. So there is a near rhyme mode that lets a few groups rhyme across the boundary.

What matters is that it is an explicit switch, not a vibe. Pick near rhyme and the checker enforces near-rhyme rules; leave it off and it enforces same-group. Both are counted, just with a different ruler. Loose can be a setting. It cannot be a lie.

Tolerance works the same way: ±1 is the reader's choice, and once chosen an eight-character line passes a seven-character constraint and a nine-character line does not.

Is it worth it

An extra round trip, an extra computation, and a rhyme table to maintain. In exchange: when the screen says seven characters a line, those seven characters were counted.

The same rule applies to the rest of what we build. Measure what can be measured, and do not put the rest on the page.

Read next

Have something in mind?

Tell us what you are building and we will tell you honestly whether we are the right studio for it.