Mubert API v3:
POST /api/v3/public/tracks(customer-id+access-token),可用playlist_index或prompt(Text-2-Music),再GET /tracks/{id}轮询至generations[].status=done。
先看清成品
- mock 创建后轮询得到 mp3 URL。
- 官方无效凭证 HTTP 401。
- 1 项 pytest 通过。
合规提醒:Mubert 曲目授权取决于套餐与合同;免费/低档常需署名且用途受限。发布前核对 license 与是否允许商用/变现平台。曲目下载 URL 会过期,及时落盘。本文不编造套餐价格。
运行说明:无正式 MUBERT_CUSTOMER_ID / ACCESS_TOKEN。A/B 用 mock;对 music-api.mubert.com 无效凭证实测 HTTP 401。C 为本地 license 门禁。2026-10-23(Asia/Shanghai)pytest 通过。
做完之后你会得到什么?
| 成果 | 说明 |
|---|---|
generate_track.py |
生成+轮询客户端 |
mock_mubert.py |
假服务 |
test_111a.py |
测试 |
需要准备哪些环境?
| 项目 | 说明 |
|---|---|
| Python | 3.10+ |
| 环境变量 | MUBERT_CUSTOMER_ID、MUBERT_ACCESS_TOKEN;可选 MUBERT_BASE_URL |
关键事实
| 事实 | 来源 |
|---|---|
| Public 目录用 customer-id + access-token;Service 目录用 company-id + license-token | 官方 API Docs |
| 优先搜 Library,再花配额生成;部分时长可秒回 | 官方 Quick Start / MCP 说明 |
| 下载链接会过期,需尽快保存 | 官方 Docs |
文件结构
111-mubert-A-src/
├── generate_track.py
├── mock_mubert.py
└── test_111a.py
第 1 步:客户端
#!/usr/bin/env python3
"""Mubert API v3: create a track then poll until generations[].status == done.
Auth (public): customer-id + access-token headers.
Prefer playlist_index OR prompt (text-2-music). Save the download URL before it expires.
"""
from __future__ import annotations
import argparse, json, os, sys, time, urllib.error, urllib.request
def _req(method, url, headers, body=None):
data = None if body is None else json.dumps(body).encode()
h = {**headers}
if data is not None:
h["Content-Type"] = "application/json"
req = urllib.request.Request(url, data=data, headers=h, method=method)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read().decode())
except urllib.error.HTTPError as e:
raise SystemExit(f"HTTP {e.code}: {e.read().decode(errors='replace')}") from e
def create_track(base, customer_id, token, *, playlist_index=None, prompt=None, duration=30, fmt="mp3", mode="track", intensity="medium"):
body = {"duration": duration, "format": fmt, "mode": mode, "intensity": intensity}
if prompt:
body["prompt"] = prompt
elif playlist_index:
body["playlist_index"] = playlist_index
else:
raise SystemExit("需要 --playlist-index 或 --prompt")
return _req("POST", f"{base.rstrip('/')}/api/v3/public/tracks",
{"customer-id": customer_id, "access-token": token}, body)
def get_track(base, customer_id, token, track_id):
return _req("GET", f"{base.rstrip('/')}/api/v3/public/tracks/{track_id}",
{"customer-id": customer_id, "access-token": token})
def wait_done(base, customer_id, token, track_id, *, timeout=30, interval=0.2):
deadline = time.time() + timeout
while time.time() < deadline:
data = get_track(base, customer_id, token, track_id)
gens = (data.get("data") or data).get("generations") or []
if gens and gens[0].get("status") == "done" and gens[0].get("url"):
return data
time.sleep(interval)
raise SystemExit("轮询超时:请检查配额或改用 webhook")
def main(argv=None) -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--base", default=os.environ.get("MUBERT_BASE_URL", "https://music-api.mubert.com"))
ap.add_argument("--customer-id", default=os.environ.get("MUBERT_CUSTOMER_ID", ""))
ap.add_argument("--token", default=os.environ.get("MUBERT_ACCESS_TOKEN", ""))
ap.add_argument("--playlist-index", default=None)
ap.add_argument("--prompt", default=None)
ap.add_argument("--duration", type=int, default=30)
ap.add_argument("--format", default="mp3", choices=["mp3", "wav"])
ap.add_argument("--mode", default="track", choices=["track", "loop", "jingle", "mix"])
ap.add_argument("--intensity", default="medium", choices=["low", "medium", "high"])
ap.add_argument("--poll", action="store_true", help="创建后轮询至 done")
args = ap.parse_args(argv)
if not args.customer_id or not args.token:
print("设置 MUBERT_CUSTOMER_ID 与 MUBERT_ACCESS_TOKEN(由 service/customers 签发)", file=sys.stderr)
return 2
created = create_track(
args.base, args.customer_id, args.token,
playlist_index=args.playlist_index, prompt=args.prompt,
duration=args.duration, fmt=args.format, mode=args.mode, intensity=args.intensity,
)
tid = (created.get("data") or created).get("id")
out = wait_done(args.base, args.customer_id, args.token, tid) if args.poll else created
print(json.dumps(out, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
第 2 步:Mock
#!/usr/bin/env python3
"""Mock Mubert public tracks API."""
from __future__ import annotations
import json, uuid
from http.server import BaseHTTPRequestHandler, HTTPServer
CID, TOK = "cust-test", "tok-test"
TRACKS = {}
class H(BaseHTTPRequestHandler):
def log_message(self, *a): pass
def _auth(self):
return self.headers.get("customer-id") == CID and self.headers.get("access-token") == TOK
def _send(self, code, obj):
b = json.dumps(obj).encode()
self.send_response(code); self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(b))); self.end_headers(); self.wfile.write(b)
def do_POST(self):
if not self._auth(): self._send(401, {"error": "Unauthorized"}); return
if self.path.rstrip("/") != "/api/v3/public/tracks":
self.send_error(404); return
n = int(self.headers.get("Content-Length", 0))
body = json.loads(self.rfile.read(n) or b"{}")
if not body.get("playlist_index") and not body.get("prompt"):
self._send(400, {"error": "playlist_index or prompt required"}); return
tid = str(uuid.uuid4())
TRACKS[tid] = {
"id": tid, "playlist_index": body.get("playlist_index"),
"prompt": body.get("prompt"), "duration": body.get("duration", 30),
"intensity": body.get("intensity", "high"), "mode": body.get("mode", "track"),
"generations": [{"session_id": "g1", "format": body.get("format", "mp3"),
"bitrate": 128, "status": "processing", "url": None}],
}
self._send(200, {"data": TRACKS[tid]})
def do_GET(self):
if not self._auth(): self._send(401, {"error": "Unauthorized"}); return
prefix = "/api/v3/public/tracks/"
if not self.path.startswith(prefix):
self.send_error(404); return
tid = self.path[len(prefix):].split("?")[0]
t = TRACKS.get(tid)
if not t: self._send(404, {"error": "not found"}); return
t = dict(t)
t["generations"] = [{**t["generations"][0], "status": "done",
"url": f"https://cdn.example/mock/{tid}.mp3"}]
TRACKS[tid] = t
self._send(200, {"data": t})
def main():
import argparse
ap = argparse.ArgumentParser(); ap.add_argument("--port", type=int, default=9111)
args = ap.parse_args()
HTTPServer(("127.0.0.1", args.port), H).serve_forever()
if __name__ == "__main__":
main()
第 3 步:测试
import json, subprocess, sys, time
from pathlib import Path
HERE = Path(__file__).resolve().parent
def test_generate_and_poll():
port = 19111
proc = subprocess.Popen([sys.executable, str(HERE/"mock_mubert.py"), "--port", str(port)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
try:
time.sleep(0.3)
r = subprocess.run(
[sys.executable, str(HERE/"generate_track.py"), "--base", f"http://127.0.0.1:{port}",
"--customer-id", "cust-test", "--token", "tok-test",
"--prompt", "calm lo-fi study beat", "--duration", "30", "--poll"],
capture_output=True, text=True, check=True)
data = json.loads(r.stdout)
gens = (data.get("data") or data)["generations"]
assert gens[0]["status"] == "done"
assert gens[0]["url"].endswith(".mp3")
finally:
proc.terminate(); proc.wait(timeout=5)
第 4 步:运行
$ pytest 111a
. [100%]
1 passed in 0.36s
$ invalid key music-api.mubert.com
HTTP 401
{"message":"Unauthenticated","code":"Unauthenticated"}
怎么检查结果?
- generations[0].status 为 done 且有 url。
- 正式调用前先跑 C 篇 license 门禁。
调试示例
401 Unauthenticated。 确认 customer/access 来自 service/customers,不是把 company/license 拿来调 public。
常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 只要文字描述 | 用 --prompt | 需合同含 ttm |
| 循环配乐 | --mode loop | 看 license modes |
| Webhook | license 上配置 | 可少轮询 |
模板规则
- 凭证分环境保管。
- 先 Library 后生成。
- URL 过期前落盘。
项目资源
| 用途 | 地址 |
|---|---|
| API Docs(官方) | https://mubert.com/api/docs |
| API 产品页 | https://mubert.com/api |
| Mubert | https://mubert.com |
生成通了,授权条款才是上线前最后一关。