Udio 的 Custom 模式允许自己写歌词,并用
[Verse]、[Chorus]等标签标出段落结构。标签拼错、副歌前后写得不一致、提示词里出现艺人名字,都会浪费生成额度。 这篇做lyrics_lint.py:对照官方标签列表检查歌词,并检查提示词中有没有出现需要避开的名字。
先看清成品
- 示例歌词:
[Breakdwon]被提示“是不是 [Breakdown]?”;两次副歌中mile is和mile's的差别被发现;提示词中的 “Example Artist” 被报错。- 修复了名字的子串误报:“Seal”不再命中“sealed”。
- pytest 6 项通过。
合规提醒:根据 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 是虚构的。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
lyrics.py |
解析歌词段落,内含官方标签列表 |
lyrics_lint.py |
检查歌词;有错误时退出码为 1 |
song.txt、avoid.txt |
示例歌词和名字列表 |
test_lint.py |
6 项测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.9 以上(box 上是 3.13);测试需要 pip install pytest |
| Udio | 网页版 https://www.udio.com,在 Create → Custom 中粘贴歌词 |
关键事实
| 事实 | 来源 |
|---|---|
Custom 模式下在歌词框按 / 可以选择标签;官方列出了 28 个标签,但说明这不是完整列表,也不是硬性指令 |
Guidance tags |
| 官方的提示词格式一般是“主题,若干个用逗号分隔的风格标签” | Guidance tags |
| Udio 会过滤对受版权保护作品和艺人声音的复制 | AI and the Future of Music |
| 下载已关闭 | UMG 合作带来的变化 |
avoid.txt 由你自己维护(例如在世艺人的名字),本文不提供名单;示例里只放了一个虚构的名字 “Example Artist”。
文件结构
67-udio-A-src/
├── lyrics.py
├── lyrics_lint.py
├── song.txt
├── avoid.txt
└── test_lint.py
第 1 步:歌词解析和官方标签
"""解析带 [段落标签] 的歌词文本,供 lint 和音节检查共用。"""
import re
# Udio 帮助中心 "Level up your creations with 'guidance tags'" 中列出的标签(官方说明不是完整列表,也不是硬性指令)
OFFICIAL_TAGS = ["Verse", "Chorus", "Intro", "Outro", "Bridge", "Hook", "Pre-chorus", "Refrain", "Post-chorus", "Drop",
"Interlude", "Instrumental Break", "Instrumental", "Build", "Pre-hook", "Pre-drop", "Pre-refrain", "Break", "All",
"Breakdown", "Instrumental Bridge", "Sample", "Solo", "Ensemble", "Post-hook", "Spoken Word", "Choir", "Announcer"]
TAG = re.compile(r"^\s*\[([^\]]+)\]\s*$")
def parse(text):
"""返回 [(tag, [lines])];标签前的歌词记在 tag=None 下。"""
sections, cur = [], (None, [])
for raw in text.splitlines():
m = TAG.match(raw)
if m:
if cur[0] is not None or cur[1]: sections.append(cur)
cur = (m.group(1).strip(), [])
elif raw.strip():
cur[1].append(raw.strip())
if cur[0] is not None or cur[1]: sections.append(cur)
return sections
def base_tag(tag):
"""'Verse 2' → 'Verse';大小写不敏感地匹配官方标签。"""
t = re.sub(r"\s*\d+$", "", tag).strip().lower()
for o in OFFICIAL_TAGS:
if o.lower() == t: return o
return None
第 2 步:检查脚本
"""在粘贴进 Udio(Create → Custom)之前检查歌词。
- 标签:对照官方 guidance tags 列表;不在列表里的只给提醒(官方说明这不是完整列表)
- 结构:开头没有标签的歌词、空段落、没有副歌
- 副歌:多次出现的 [Chorus] 文字不一致时提醒(可能是笔误)
- 提示词:--avoid 传入你自己维护的名字列表(例如在世艺人),出现就报错。Udio 表示会过滤对艺人声音和受版权保护作品的复制,
最好一开始就用风格、乐器、情绪来描述,而不是写某位艺人的名字。"""
import argparse, difflib, re, sys
from lyrics import parse, base_tag, OFFICIAL_TAGS
def lint(text, prompt="", avoid=()):
secs = parse(text); errs, warns = [], []
if not secs: errs.append("歌词为空")
for i, (tag, lines) in enumerate(secs):
if tag is None: warns.append(f"开头有 {len(lines)} 行歌词没有段落标签"); continue
if base_tag(tag) is None:
near = difflib.get_close_matches(re.sub(r"\s*\d+$", "", tag), OFFICIAL_TAGS, n=1, cutoff=0.75)
warns.append(f"[{tag}] 不在官方标签列表中" + (f",是不是 [{near[0]}]?" if near else "(可以用,但效果没有保证)"))
if not lines and base_tag(tag) not in ("Instrumental", "Instrumental Break", "Instrumental Bridge", "Interlude", "Break", "Drop", "Solo", "Intro", "Outro", "Breakdown", "Build"):
warns.append(f"[{tag}] 是空段落")
for l in lines:
if len(l.split()) > 14: warns.append(f"[{tag}] 这一行有 {len(l.split())} 个词,可能唱不完:{l[:40]}…")
chor = [tuple(l) for t, l in secs if t and base_tag(t) == "Chorus"]
if secs and not chor: warns.append("没有 [Chorus]")
if len(set(chor)) > 1: warns.append(f"{len(chor)} 次副歌的文字不完全一致,确认是否有意")
low = (prompt + "\n" + text).lower()
for name in avoid:
# 按整词匹配:避免 "Seal" 命中 "sealed"(调试时实际遇到的误报)
if name.strip() and re.search(r"(?<![\w'])" + re.escape(name.strip().lower()) + r"(?![\w'])", low): errs.append(f"提示词或歌词中出现了避免使用的名字:{name.strip()}")
return errs, warns
def main(argv=None):
ap = argparse.ArgumentParser(); ap.add_argument("lyrics"); ap.add_argument("--prompt", default="")
ap.add_argument("--avoid", help="每行一个名字的文本文件"); a = ap.parse_args(argv)
avoid = open(a.avoid, encoding="utf-8").read().splitlines() if a.avoid else []
errs, warns = lint(open(a.lyrics, encoding="utf-8").read(), a.prompt, avoid)
for e in errs: print("[ERROR]", e)
for w in warns: print("[WARN]", w)
print("OK" if not errs and not warns else f"{len(errs)} 个错误,{len(warns)} 条提醒")
return 1 if errs else 0
if __name__ == "__main__":
sys.exit(main())
第 3 步:示例歌词(我自己写的)
[Intro]
[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 a song on our lips
Nobody's waiting and nobody knows
Where the long road goes when the evening glows
[Chorus]
Turn it up, let the summer in
Every mile's a place to begin
[Breakdwon]
[Outro]
Every mile is a place to begin
第 4 步:测试
import lyrics, lyrics_lint as L
SONG = open("song.txt", encoding="utf-8").read()
def test_parse():
secs = lyrics.parse(SONG)
assert [t for t, _ in secs] == ["Intro", "Verse 1", "Chorus", "Verse 2", "Chorus", "Breakdwon", "Outro"]
assert len(secs[1][1]) == 4 and secs[0][1] == []
def test_song_findings():
errs, warns = L.lint(SONG, "upbeat indie pop, like Example Artist", ["Example Artist"])
assert errs == ["提示词或歌词中出现了避免使用的名字:Example Artist"]
assert any("是不是 [Breakdown]" in w for w in warns)
assert any("副歌的文字不完全一致" in w for w in warns)
assert not any("[Intro] 是空段落" in w for w in warns) # 前奏可以没有歌词
def test_case_and_numbers():
assert lyrics.base_tag("verse 2") == "Verse" and lyrics.base_tag("PRE-CHORUS") == "Pre-chorus"
assert lyrics.base_tag("Rap") is None
def test_untagged_and_no_chorus():
errs, warns = L.lint("just some words\n[Verse]\nmore words")
assert any("没有段落标签" in w for w in warns) and any("没有 [Chorus]" in w for w in warns) and errs == []
def test_long_line():
_, w = L.lint("[Chorus]\n" + " ".join(["la"] * 20))
assert any("20 个词" in x for x in w)
def test_avoid_whole_word():
assert L.lint("[Chorus]\nour fate is sealed tonight", "pop", ["Seal"])[0] == []
assert L.lint("[Chorus]\nla la", "in the style of seal", ["Seal"])[0]
第 5 步:运行
$ python lyrics_lint.py song.txt --prompt "upbeat indie pop, bright guitars, like Example Artist" --avoid avoid.txt
[ERROR] 提示词或歌词中出现了避免使用的名字:Example Artist
[WARN] [Breakdwon] 不在官方标签列表中,是不是 [Breakdown]?
[WARN] [Breakdwon] 是空段落
[WARN] 2 次副歌的文字不完全一致,确认是否有意
1 个错误,3 条提醒
$ python -m pytest -q
6 passed
怎么检查结果?
- 修正标签拼写,删除提示词里的艺人名字,改为描述风格,例如
summer road trip, indie pop, bright guitars。 - 再运行一次,应该只剩下你有意保留的提醒。
- 然后在 Udio 的 Create 页面选择 Custom,粘贴歌词。(本文没有实际操作这一步。)
调试示例
名字的子串误报。 第一版检查名字时用的是 name.lower() in text。我用一个短名字测试:避开列表里是 “Seal”,歌词是 “our fate is sealed tonight”。结果报错,说出现了避免使用的名字,而歌词里只有 sealed。
修复:改为整词匹配(前后不能紧跟字母、数字或撇号)。现在 “in the style of seal” 仍然会报错,sealed 不会。两种情况都加进了测试。
标签拼写提示。 第一版只报“不在官方列表中”,看到 [Breakdwon] 时还要自己去找正确的写法。后来加了 difflib 的相似度匹配(阈值 0.75),会直接给出 [Breakdown]。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 自定义标签被提醒 | 不在官方列表中 | 官方说明可以试用其他标签,提醒可以忽略 |
| 名字报错 | 提示词里写了避免使用的名字 | 改为描述风格、乐器和情绪 |
| 副歌不一致 | 有笔误,或者有意修改 | 确认后保留或统一 |
| 生成结果不按标签走 | 标签只是引导,由 AI 最终决定 | 多生成几次,或调整歌词 |
模板规则
- 提示词描述风格,不写艺人名字。
- 只用自己写的歌词。
- 先过一遍 lint,再花生成额度。
项目资源
| 用途 | 地址 |
|---|---|
| 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 |
| AI and the Future of Music | https://help.udio.com/en/articles/11664414-ai-and-the-future-of-music |
标签写对、名字避开,生成额度就花在真正的创作上。