设计实践

Udio 的 Custom 模式允许自己写歌词,并用 [Verse]、[Chorus] 等标签标出段落结构

Udio 的 Custom 模式允许自己写歌词,并用 [Verse]、[Chorus] 等标签标出段落结构。标签拼错、副歌前后写得不一致、提示词里出现艺人名字,都会浪费生成额度。 这篇做 lyricslint.py:对照官方标签列表检查歌词,并检查提示词中有没有出现需要避开的名字。

BINBHO · 图文教程

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

标签写对、名字避开,生成额度就花在真正的创作上。