初始发布: 21 个 skills (Claude Code / Codex / DSH)

This commit is contained in:
2026-08-14 01:51:44 +08:00
commit ea0857bedb
129 changed files with 35566 additions and 0 deletions
+179
View File
@@ -0,0 +1,179 @@
#!/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
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_id(24 小时有效)")
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 返回而非 url(url 链接 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()