第二段主歌通常沿用第一段的旋律。如果某一行的音节比第一段多出一倍,生成的演唱往往会挤成一团。 这篇做
meter.py:用 CMU 发音词典计算每行的音节数,逐行比较同类段落,在粘贴进 Udio 之前先把歌词“对齐”。
先看清成品
song.txt的主歌和副歌音节基本对齐。song_v2.txt中 Verse 2 的第 2 行是 21 个音节(参考 10),第 3 行是 5 个音节,都被报出。- 修复了估算偏差:glovebox 从 3 个音节修正为 2 个,mile's 从 2 个修正为 1 个。pytest 5 项通过。
合规提醒:根据 Udio 帮助中心,自 2025-10-29 与 Universal Music Group 合作起,音频、视频和分轨的下载已经关闭,歌曲只能通过 udio.com 链接分享。本文因此不涉及任何下载、录制或转换 Udio 音频的做法。网上那些“Udio 下载器”和录屏方法违反官方规则,不要使用。提示词里不要写在世艺人的名字,也不要使用他人受版权保护的歌词;Udio 表示会过滤对艺人声音和版权作品的复制。不共享账号。本文不列价格,套餐和额度以官网为准。
运行说明:box 上没有 Udio 账号,没有生成过歌曲。本地脚本和测试都在 box 上真实运行过(Python 3.13;C 篇使用 pronouncing 0.3.0 和 CMU 发音词典)。示例歌词是我自己写的;示例中的 Udio 链接 ID 是虚构的。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
meter.py |
--show 显示每行音节数;有偏差时退出码为 1 |
lyrics.py |
与 A 篇相同 |
song.txt、song_v2.txt |
对齐和不对齐的示例 |
test_meter.py |
5 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.9 以上(box 上是 3.13);测试需要 pip install pytest |
| Udio | 网页版 https://www.udio.com,在 Create → Custom 中粘贴歌词 |
| 依赖 | pip install pronouncing(内含 CMU 发音词典) |
关键事实
| 事实 | 来源 |
|---|---|
Custom 模式下在歌词框按 / 可以选择标签;官方列出了 28 个标签,但说明这不是完整列表,也不是硬性指令 |
Guidance tags |
| 音节数来自 CMU Pronouncing Dictionary,查不到的词用规则估算 | pronouncing 库文档 |
音节对齐只是写词时的参考,Udio 怎样演唱由模型决定。只适用于英文歌词。
文件结构
67-udio-C-src/
├── meter.py
├── lyrics.py
├── song.txt
├── song_v2.txt
└── test_meter.py
第 1 步:音节比较脚本
"""比较同类段落(Verse 1 与 Verse 2、每次副歌)每行的音节数。第二段主歌通常沿用第一段的旋律,
音节数相差太多时,生成的演唱容易挤在一起或拖长。音节数来自 CMU 发音词典(pronouncing 库),
词典里没有的词用元音组规则估算,并标出 '~'。只适用于英文歌词。"""
import argparse, re, sys
import pronouncing
from lyrics import parse, base_tag
def word_syll(w):
w = w.lower().strip("'")
ph = pronouncing.phones_for_word(w)
if ph: return pronouncing.syllable_count(ph[0]), True
if w.endswith("'s") and pronouncing.phones_for_word(w[:-2]): # mile's → mile('s 不加音节,-ce/-se 等例外忽略)
return pronouncing.syllable_count(pronouncing.phones_for_word(w[:-2])[0]), True
for k in range(3, len(w) - 2): # 复合词:glovebox → glove + box
p1, p2 = pronouncing.phones_for_word(w[:k]), pronouncing.phones_for_word(w[k:])
if p1 and p2: return pronouncing.syllable_count(p1[0]) + pronouncing.syllable_count(p2[0]), True
groups = re.findall(r"[aeiouy]+", w)
n = len(groups) - (1 if w.endswith("e") and not w.endswith(("le", "ee")) and len(groups) > 1 else 0)
return max(n, 1), False
def line_syll(line):
total, exact = 0, True
for w in re.findall(r"[A-Za-z']+", line):
n, ok = word_syll(w); total += n; exact &= ok
return total, exact
def compare(text, tol=2):
groups = {}
for tag, lines in parse(text):
if tag and lines: groups.setdefault(base_tag(tag) or tag, []).append((tag, lines))
report = []
for kind, secs in groups.items():
if len(secs) < 2: continue
ref_tag, ref = secs[0]
for tag, lines in secs[1:]:
for i in range(max(len(ref), len(lines))):
a = line_syll(ref[i]) if i < len(ref) else None; b = line_syll(lines[i]) if i < len(lines) else None
if a is None or b is None:
report.append((kind, tag, i + 1, a, b, "行数不同")); continue
if abs(a[0] - b[0]) > tol:
report.append((kind, tag, i + 1, a, b, f"相差 {abs(a[0] - b[0])} 个音节"))
return report
def fmt(x): return "-" if x is None else f"{x[0]}{'' if x[1] else '~'}"
def main(argv=None):
ap = argparse.ArgumentParser(); ap.add_argument("lyrics"); ap.add_argument("--tol", type=int, default=2); ap.add_argument("--show", action="store_true")
a = ap.parse_args(argv); text = open(a.lyrics, encoding="utf-8").read()
if a.show:
for tag, lines in parse(text):
for l in lines: print(f"[{tag}] {fmt(line_syll(l)):>4} {l}")
rep = compare(text, a.tol)
for kind, tag, n, x, y, why in rep: print(f"[{tag}] 第 {n} 行:参考 {fmt(x)},本段 {fmt(y)} —— {why}")
print("音节基本对齐" if not rep else f"{len(rep)} 行需要调整")
return 1 if rep else 0
if __name__ == "__main__":
sys.exit(main())
第 2 步:不对齐的示例
[Verse 1]
Windows down on the coastal road
Maps in the glovebox we never unfold
Gas station coffee and a borrowed tune
We'll be at the ocean by the afternoon
[Chorus]
Turn it up, let the summer in
Every mile is a place to begin
[Verse 2]
Sunburned arms and a cassette that skips
Salt in the air and the radio playing every song we used to know by heart
Nobody's waiting
Where the long road goes when the evening glows
[Chorus]
Turn it up, let the summer in
Every mile's a place to begin
第 3 步:测试
import meter as M
def test_dictionary_words():
assert M.word_syll("summer") == (2, True) and M.word_syll("afternoon") == (3, True)
def test_fallbacks_fixed_in_debugging():
# 第一版用元音组估算:glovebox=3、mile's=2,都多算了 1
assert M.word_syll("glovebox") == (2, True) and M.word_syll("mile's") == (1, True)
assert M.word_syll("zxqvbrt")[1] is False # 真正查不到的词标记为估算
def test_song_aligned():
assert M.compare(open("song.txt", encoding="utf-8").read()) == []
def test_song_v2_mismatch():
rep = M.compare(open("song_v2.txt", encoding="utf-8").read())
assert [(r[1], r[2]) for r in rep] == [("Verse 2", 2), ("Verse 2", 3)]
assert rep[0][4][0] - rep[0][3][0] > 2
def test_line_count_difference():
rep = M.compare("[Verse 1]\na b\nc d\n[Verse 2]\na b\n")
assert rep[0][5] == "行数不同"
第 4 步:运行
$ python meter.py song.txt --show
[Verse 1] 8 Windows down on the coastal road
[Verse 1] 10 Maps in the glovebox we never unfold
[Verse 1] 10 Gas station coffee and a borrowed tune
...
音节基本对齐
$ python meter.py song_v2.txt
[Verse 2] 第 2 行:参考 10,本段 21 —— 相差 11 个音节
[Verse 2] 第 3 行:参考 10,本段 5 —— 相差 5 个音节
2 行需要调整
$ python -m pytest -q
5 passed
怎么检查结果?
- 带
~的数字是估算值,需要自己数一下。 - 默认允许相差 2 个音节(
--tol);节奏自由的曲风可以调大。 - 行数不同时也会报出。
调试示例
估算规则多算了音节。 第一次运行 --show 时,有两行带 ~(表示词典里查不到、用规则估算):
[Verse 1] 11~ Maps in the glovebox we never unfold
[Chorus] 10~ Every mile's a place to begin
自己数一下,两行实际是 10 和 9。原因是:
- glovebox 是复合词,词典里没有;元音组规则把 glove 中不发音的 e 也算成了一个音节,得到 3(实际是 2);
- mile's 的
's使结尾的 e 不再是最后一个字母,“去掉结尾不发音 e”的规则没有生效,得到 2(实际是 1)。
修复:带 's 的词先查去掉 's 后的原词;查不到的词尝试拆成两个词典里都有的词(glove + box)。修复后两行都变成精确值,不再带 ~。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
出现很多 ~ |
用了俚语、人名或自造词 | 自己数一下,或者换个写法 |
| 报“行数不同” | 两段主歌的行数不一样 | 补齐,或确认是有意为之 |
| 中文歌词 | CMU 词典只收录英文 | 中文可以按字数比较,本工具不支持 |
| 对齐了但唱得仍然不顺 | 重音和韵脚也会影响 | 多生成几次,或在 Udio 中重新生成该段 |
模板规则
- 同类段落的每一行音节数尽量接近。
- 带
~的估算值要人工确认。 - 对齐是参考,不是硬性要求。
项目资源
| 用途 | 地址 |
|---|---|
| Guidance tags(官方标签列表) | https://help.udio.com/en/articles/11166249-level-up-your-creations-with-guidance-tags |
| UMG 合作带来的变化(下载已关闭) | https://help.udio.com/en/articles/12683565-changes-associated-with-the-universal-music-group-umg-partnership |
| pronouncing 库文档 | https://pronouncing.readthedocs.io/ |
写词时先把音节对齐,生成出来的演唱更容易贴合旋律。