Files
skills/minimax-music-codex/scripts/generate.py
T

185 lines
7.7 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""MiniMax 音乐生成 CLIPython 3 标准库,无需安装依赖)。
用法示例:
# 带唱
python3 generate.py "独立民谣,忧郁内省,木吉他+弦乐" -l @lyrics.txt
# 纯音乐
python3 generate.py "中世纪奇幻大地图,鲁特琴+竖琴,无鼓" --instrumental
# 自动作词
python3 generate.py "抒情流行,夏夜告别" --auto-lyrics
# 翻唱
python3 generate.py "" --cover ref.mp3 -l "[Verse]\n新歌词..."
"""
import argparse, base64, json, os, sys, time, urllib.request, urllib.error
API_URL = "https://api.minimaxi.com/v1/music_generation"
HERE = os.path.dirname(os.path.abspath(__file__))
PAID = {"music-3.0", "music-2.6", "music-cover"}
FREE = {"music-3.0-free", "music-2.6-free", "music-cover-free"}
MODELS = sorted(PAID | FREE)
# base_resp.status_code -> 人话
ERRORS = {
1002: "触发限流(付费模型 RPM 120,free 模型 RPM 3)。等一会儿再试,别并发。",
1004: "鉴权失败:API Key 无效。检查 MINIMAX_API_KEY 或 key.txt。",
1008: "账户余额不足,去控制台充值。",
1026: "命中敏感内容审核,改一下 prompt 或歌词。",
2013: "参数不合法(看 status_msg 里的具体字段)。",
2049: "API Key 格式不对。",
}
def load_key():
k = os.environ.get("MINIMAX_API_KEY", "").strip()
if k:
return k
config_path = os.path.expanduser("~/.config/minimax_api_key")
if os.path.exists(config_path):
k = open(config_path).read().strip()
if k:
return k
p = os.path.join(HERE, "key.txt")
if os.path.exists(p):
k = open(p).read().strip()
if k:
return k
sys.exit("没有 API Key。设置环境变量 MINIMAX_API_KEY,或写入 %s" % p)
def read_maybe_file(v):
"""支持 @path 从文件读取。"""
if v and v.startswith("@"):
return open(os.path.expanduser(v[1:]), encoding="utf-8").read()
return v
def main():
ap = argparse.ArgumentParser(description="MiniMax 音乐生成")
ap.add_argument("prompt", nargs="?", default="",
help="曲风/情绪/场景描述,<=2000 字。纯音乐时必填")
ap.add_argument("-l", "--lyrics", default="",
help="歌词,<=3500 字,用 \\n 分行;支持 @文件路径。带唱时必填")
ap.add_argument("--instrumental", action="store_true", help="生成纯音乐(无人声)")
ap.add_argument("--auto-lyrics", action="store_true",
help="让模型按 prompt 自动作词(lyrics_optimizer")
ap.add_argument("--cover", default="",
help="翻唱参考音频:本地文件路径 或 http(s) URL6s-6min<=50MB")
ap.add_argument("--cover-feature-id", default="",
help="翻唱预处理接口拿到的 feature_id24 小时有效)")
ap.add_argument("-m", "--model", default="music-3.0", choices=MODELS)
ap.add_argument("--free", action="store_true",
help="改用对应的 -free 免费模型(RPM 3,不计费)")
ap.add_argument("-o", "--output", default="", help="输出路径,默认按时间戳命名")
ap.add_argument("--sample-rate", type=int, default=44100,
choices=[16000, 24000, 32000, 44100])
ap.add_argument("--bitrate", type=int, default=256000,
choices=[32000, 64000, 128000, 256000])
ap.add_argument("--format", default="mp3", choices=["mp3", "wav", "pcm"])
ap.add_argument("--watermark", action="store_true", help="加 AIGC 水印")
ap.add_argument("--hex", action="store_true",
help="用 hex 返回而非 urlurl 链接 24 小时过期)")
ap.add_argument("--json", action="store_true", help="打印完整 JSON 返回")
a = ap.parse_args()
model = a.model
if a.free and not model.endswith("-free"):
model += "-free"
lyrics = read_maybe_file(a.lyrics)
is_cover = bool(a.cover or a.cover_feature_id)
if is_cover and not model.startswith("music-cover"):
model = "music-cover-free" if model.endswith("-free") else "music-cover"
# ---- 本地前置校验:省得白花钱 ----
if a.cover and a.cover_feature_id:
sys.exit("--cover 和 --cover-feature-id 互斥,只能给一个")
if a.instrumental and not a.prompt.strip():
sys.exit("纯音乐模式下 prompt 必填(要靠它定曲风和配器)")
if not a.instrumental and not is_cover and not a.auto_lyrics and not lyrics.strip():
sys.exit("带唱模式下 lyrics 必填。要么给 -l,要么加 --instrumental"
"要么加 --auto-lyrics 让模型自己写")
if a.cover_feature_id and not (10 <= len(lyrics.strip()) <= 1000):
sys.exit("带 feature_id 的翻唱要求歌词 10-1000 字,当前 %d 字" % len(lyrics.strip()))
if len(a.prompt) > 2000:
sys.exit("prompt 超长:%d > 2000 字" % len(a.prompt))
if len(lyrics) > 3500:
sys.exit("lyrics 超长:%d > 3500 字" % len(lyrics))
body = {
"model": model,
"output_format": "hex" if a.hex else "url",
"audio_setting": {"sample_rate": a.sample_rate, "bitrate": a.bitrate,
"format": a.format},
}
if a.prompt.strip():
body["prompt"] = a.prompt
if lyrics.strip():
body["lyrics"] = lyrics
if a.instrumental:
body["is_instrumental"] = True
if a.auto_lyrics:
body["lyrics_optimizer"] = True
if a.watermark:
body["aigc_watermark"] = True
if a.cover:
if a.cover.startswith("http"):
body["audio_url"] = a.cover
else:
with open(os.path.expanduser(a.cover), "rb") as f:
body["audio_base64"] = base64.b64encode(f.read()).decode()
if a.cover_feature_id:
body["cover_feature_id"] = a.cover_feature_id
cost = "免费" if model.endswith("-free") else "¥1.0"
print("模型 %s%s)· 提交中…" % (model, cost), flush=True)
req = urllib.request.Request(
API_URL, data=json.dumps(body, ensure_ascii=False).encode("utf-8"),
headers={"Authorization": "Bearer " + load_key(),
"Content-Type": "application/json"}, method="POST")
t = time.time()
try:
r = json.loads(urllib.request.urlopen(req, timeout=600).read().decode("utf-8"))
except urllib.error.HTTPError as e:
sys.exit("HTTP %d %s" % (e.code, e.read().decode("utf-8", "replace")))
except urllib.error.URLError as e:
sys.exit("网络错误:%s" % e)
if a.json:
print(json.dumps(r, ensure_ascii=False, indent=2))
# HTTP 200 不代表成功,一律看 base_resp.status_code
base = r.get("base_resp") or {}
code = base.get("status_code")
if code != 0:
sys.exit("生成失败 %s: %s\n%s" % (code, base.get("status_msg"),
ERRORS.get(code, "")))
info = r.get("extra_info") or {}
dur = info.get("music_duration", 0) / 1000.0
print("成功 · 时长 %.1fs · %s Hz · %s 声道 · %s bps · 耗时 %.0fs"
% (dur, info.get("music_sample_rate"), info.get("music_channel"),
info.get("bitrate"), time.time() - t), flush=True)
audio = (r.get("data") or {}).get("audio")
if not audio:
sys.exit("返回里没有音频数据")
out = a.output or "minimax-music-%s.%s" % (time.strftime("%Y%m%d-%H%M%S"), a.format)
out = os.path.expanduser(out)
d = os.path.dirname(os.path.abspath(out))
if d:
os.makedirs(d, exist_ok=True)
if audio.startswith("http"):
urllib.request.urlretrieve(audio, out)
else:
with open(out, "wb") as f:
f.write(bytes.fromhex(audio))
print("已保存: %s (%.1f MB)" % (out, os.path.getsize(out) / 1e6))
if __name__ == "__main__":
main()