跳转到内容
搜索文档

做出第一段音乐

用一份温暖的民谣流行配方开始 Morning song 项目。先得到可试听的 Candidate,再检查曲谱,决定是否保存。

一份约 35 秒的 FLAC 音频,以及同次生成的 ABC 曲谱。这个教程使用真实生成已验证过的风格、原创歌词和 seed;首页 MP3 就是当时的压缩试听副本。重新运行的质量与耗时仍需实际检查。

开始时可以没有 Project。先按准备指南启动已就绪的真实 Runtime 与应用 API;如果只想练习保存流程,可以用 fake 模式,但听到的是测试音调。

这个配方先说明语言与类型,再给人声、两种主要乐器和节奏。清晰的重点比堆叠相互冲突的风格词更方便调整。

已验证风格 · style

English, gentle folk pop, warm clear voice, acoustic guitar and piano, light bass and drums, 96 BPM

先保持原样。想换成电子或更安静的编曲,放到下一章单独尝试;BPM 与风格词是创作意图,不能保证输出逐项精确遵循。

这个例子写早晨与陪伴。使用简短句子,分成 Verse 与 Chorus。35 秒只能承载短片段,不要求唱完每一句;先听语言与句子是否自然。

原创歌词 · lyrics

[Verse]
Morning gathers on the window
Quiet streets begin to glow
I will carry one small promise
Every step can help it grow
[Chorus]
Walk with me into the daylight
Let the little river flow
We can make a brighter moment
With the kindness that we show

完整示例: services/api/examples/creator/morning-song-lyrics.txt ↗

这份原创歌词与配方已经保存在受版本控制的示例中。先保留原句和段落标记,便于下一章只改一句。

使用 seed 2026192201;片段长度可留空自动(跟随歌词)或填写上限秒数,本教程沿用已验证的 max_seconds=35。把 Project 命名为 Morning song;后面的变体继续放在这个 Project 中。

打开正式 Web 的“音乐生成”,填入上面的风格、歌词与种子,选择“生成一段音乐”。按工作区生成指南检查任务、试听、看谱,再选择“保存为版本”。没有百分比时看阶段名称即可;暂时没变化不代表要重复提交。需要命令行操作时再展开下面的补充。

补充操作入口:使用现成 CLI

在仓库根目录、已启动 API 的第二个终端执行。示例会创建所需下载目录并使用上面的受控歌词。命令会创建 Project、向已配置 Runtime 提交生成,并下载 audio.flac 与 score.abc;不会保存 Version。输出目录必须尚不存在。

终端窗口
uv run --project services/api --no-sync python services/api/examples/generate_save.py generate --style "English, gentle folk pop, warm clear voice, acoustic guitar and piano, light bass and drums, 96 BPM" --lyrics-file services/api/examples/creator/morning-song-lyrics.txt --seed 2026192201 --output-dir data/morning-candidate-01

命令直接调用现成 CLI,读取上面的受控歌词。需要查阅工具行为时,完整受控源码如下。

"""Run from the repository root with the independent API project's Python."""
import argparse
import json
from pathlib import Path
import time
import httpx
def main() -> None:
parser = argparse.ArgumentParser(description="Generate an unsaved Candidate or explicitly save one as a Version.")
parser.add_argument("command", choices=["generate", "from-score", "save"])
parser.add_argument("--url", default="http://127.0.0.1:8000")
parser.add_argument("--project-id")
parser.add_argument("--style")
parser.add_argument("--lyrics-file", type=Path)
parser.add_argument("--seed", type=int, default=2026192201)
parser.add_argument("--output-dir", type=Path)
parser.add_argument("--candidate-id")
parser.add_argument("--name")
parser.add_argument("--parent-version-id")
parser.add_argument("--source-score-id")
parser.add_argument("--abc-file", type=Path)
args = parser.parse_args()
if args.command in {"generate", "from-score"} and (not args.style or not args.lyrics_file or not args.output_dir):
parser.error("generation requires --style, --lyrics-file, and --output-dir")
if args.command == "from-score" and (not args.project_id or not args.source_score_id or not args.abc_file):
parser.error("from-score requires --project-id, --source-score-id, and --abc-file")
if args.command == "save" and (not args.project_id or not args.candidate_id or not args.name):
parser.error("save requires --project-id, --candidate-id, and --name")
if args.command in {"generate", "from-score"} and args.output_dir.exists():
parser.error("use a new output directory to preserve previous downloads")
with httpx.Client(base_url=args.url, timeout=15) as client:
project_id = args.project_id
if args.command in {"generate", "from-score"}:
if project_id is None:
response = client.post("/projects", json={"name": "Morning song"})
response.raise_for_status()
project_id = response.json()["id"]
base = "/projects/" + project_id
inputs = {"style": args.style, "lyrics": args.lyrics_file.read_text(encoding="utf-8"), "seed": args.seed}
if args.command == "from-score":
inputs.update(abc=args.abc_file.read_text(encoding="utf-8"), source_score_id=args.source_score_id, parent_version_id=args.parent_version_id)
response = client.post(base + "/jobs/" + ("generate-from-score" if args.command == "from-score" else "generate"), json=inputs)
response.raise_for_status()
job_id = response.json()["id"]
deadline = time.monotonic() + 180
while True:
response = client.get(base + "/jobs/" + job_id)
response.raise_for_status()
job = response.json()
if job["status"] in {"completed", "failed", "cancelled"}:
break
if time.monotonic() >= deadline:
raise RuntimeError("Wait expired; inspect Project " + project_id + " Job " + job_id + " before retrying.")
time.sleep(0.2)
if job["status"] != "completed":
raise RuntimeError(json.dumps(job, ensure_ascii=False))
response = client.get(base + "/candidates/" + job["result"]["candidate_id"])
response.raise_for_status()
candidate = response.json()
args.output_dir.mkdir(parents=True)
for name, asset_id in [("audio.flac", candidate["audio_asset_id"]), ("score.abc", candidate["output_snapshot"]["score"]["abc_asset_id"])]:
response = client.get(base + "/assets/" + asset_id + "/content")
response.raise_for_status()
(args.output_dir / name).write_bytes(response.content)
value = {"project_id": project_id, "candidate": candidate, "output_dir": str(args.output_dir)}
else:
intent = {"candidate_id": args.candidate_id, "name": args.name, "parent_version_id": args.parent_version_id}
response = client.post("/projects/" + project_id + "/versions", json=intent)
response.raise_for_status()
value = {"project_id": project_id, "version": response.json()}
print(json.dumps(value, indent=2, ensure_ascii=False))
if __name__ == "__main__":
main()

完整示例: services/api/examples/generate_save.py ↗

  1. 在候选结果选择“试听这段音乐”,等待底部播放器读取,再选择“播放”。先完整听一次,再听人声是否可懂、伴奏是否贴合情绪、结尾有没有突兀中断。也可以播放已下载的 audio.flac。
  2. 选择“查看乐谱”,对照音频中的旋律和节奏,记下一处想继续尝试的地方。下载的 score.abc 也可在自己的 ABC 阅读工具中打开。
  3. 记下“我想保留什么”和“下一次只改什么”。声音能播放与文件校验通过,不等于音乐一定满意。

这时它仍是 Candidate。生成提供音频与 ABC;参考音频转谱提供原 MIDI,也可在内置编辑器中修改 ABC、试听并导出对应 MIDI。

给这个结果一个能记住意图的名字,例如“Morning · warm folk”。保存会留下这次输入、结果与来源信息;以后试其他配方不会覆盖这份音频或曲谱。

在同一个 Project 留下第二份 Version 后,进入“版本”,在“版本 A”和“版本 B”选择这两份作品,再选择“使用这对版本”。用底部播放器回听相同秒数的位置,记下哪一份更贴近你的目标。未保存 Candidate 可以单独试听,明确保存后才进入比较选项;只有一份版本时仍可单独试听。具体操作与恢复方法见A/B 比较玩法。

还不满意就保留 Candidate 继续尝试,不必为了每一次生成都保存 Version。用同一 Candidate、名称和父版本重复保存会返回同一 Version。

当前保存入口:Candidate → Version

将占位值替换为上一步终端输出中的实际 id。这个命令向相同 Project 写入 Version。

终端窗口
uv run --project services/api --no-sync python services/api/examples/generate_save.py save --project-id <PROJECT_ID> --candidate-id <CANDIDATE_ID> --name "Morning - warm folk"

如果等待超时,先找回同一次结果

Section titled “如果等待超时,先找回同一次结果”

CLI 等待超时会提示 Project 与 Job id。先在本地 Swagger 的“读取 Job”操作中查询这两个 id,查看已完成结果、排队状态或错误。不要直接再跑一次生成命令:原任务可能仍在执行。

若任务明确 failed,阅读 error 与 recovery_required。先解决原因;无法确认是否已执行时,请让 Runtime 管理者核对。确认可以重试后再显式提交新任务,原 Job 与素材继续保留。

下一步:同一首歌词,换一种味道

Section titled “下一步:同一首歌词,换一种味道”

保留这个 Project、原歌词、seed 和喜欢的 Version。下一章只改一个输入,让变化更容易听清。想亲手改旋律时,进入乐谱编辑与重新生成:先改一句、试听并选定,再保留带来源父版本的新作品。

源码版本: 1ce3268正文来源: docs/learn/first-music.md