本文へ移動
cccskills
無料GitHub で公開

llm-midi-composition

当使用者说"让 AI 写谱不要直接生成音频"、"做一个 AI 作曲的后端"、"LLM 输出 MIDI 怎么做"、"生成的音符老是越界/被截断"、"要能导出 .mid 给 DAW"时使用。

用 LLM(BYOK / OpenAI 兼容接口)从零生成可用的管弦乐 MIDI 与分轨音频:两级调用(整曲和声蓝图 + 逐乐器音符表)、提示词结构、被 max_tokens 截断时的残稿抢救、确定性校验修复、渲染与 MIDI 导出,以及全程的规则作曲降级策略。降级事实必须如实写进元数据,不假装是模型写的。

参考实现是 MaestrWave 的 M7 写谱演奏模式。

インストール方法を見る

含まれるファイル(1)

  • SKILL.md11.4 KB

SKILL.md(原文)

インストールする前に、エージェントに与えられる指示の中身を確認できます。

LLM 生成 MIDI 的完整方法

从「调音频模型要一段 WAV」换成「AI 先写谱、采样器再演奏」时,产物仍是每件乐器一条音轨,下游(浏览/播放/混音/指挥)无感知,同时谱子落盘可导出 MIDI。核心是一条流水线:

蓝图(LLM 写和声走向)→ 分谱(LLM 每件乐器写音符)→ 校验修复 → 渲染成音轨 → 落盘 + 导出 .mid

何时用这个 skill

  • 需要让 LLM 输出结构化乐谱(音符表)而非直接产音频
  • 需要多件乐器可对齐:精确的拍网格、各声部长度一致、能按段落进出场、能局部重绘
  • 需要谱子可导出为 .mid 供 DAW / MuseScore 使用
  • 生成链路要能容忍 LLM 输出被截断、格式残缺,且任何失败都优雅降级

不适用于 / Not for:

  • 写歌词 → 用 adversarial-lyric-writer 那一组
  • 只想要一段成品音频、不需要可编辑的谱 → 直接调音频生成模型更省事
  • 需要精确复现某首现成曲子 → 这条链路是生成新素材的,不做扒谱

核心设计原则

  1. 两级调用,不做"一次生成整曲":先让 LLM 写整曲和声走向(少量 token、失败成本低),再为每件乐器单独写音符(失败只影响一件,且天然支持并发/重试单件)。
  2. 结构化输出 + 宽松解析:提示词要求严格 JSON 形状,但解析层绝不假设输出完整合法。被截断是常态,要能从残稿里抢救。
  3. 确定性校验兜底:LLM 的几百上千个音符必然有出格项,输出不直接落盘,先过一套确定性修复(音域/复音/重叠/鼓件/小节范围),修了什么如实记进 warnings。
  4. 失败降级而非报错:用户点生成是想要音乐,不是想看 502。任何一步失败退回规则作曲,并把降级原因写进结果元数据,前端如实展示"这一条是本地作曲写的",绝不假装是模型输出。
  5. 压缩上下文防 token 爆炸:写到第八件乐器时输入里已堆了七份谱子,必须压缩已有声部视图。

第一步:蓝图 —— LLM 写整曲和声走向

用 chat_json(严格 JSON + 自动重试,temperature 偏低如 0.5),失败可退回规则和声(它只是分谱的参考,不该阻塞整曲)。

输入:调性、拍号、BPM、总小节数、风格描述、段落结构(每段的起始-结束小节 / 强度 / 是否高潮)、调内可用三和弦(允许 7 / maj7 / m7 / sus4 / 转位)。

输出要求:{"chords": ["每小节一个和弦符号", ...]},数组长度必须等于总小节数;每段最后一小节收束、全曲结尾回主和弦。

配套机制:

  • 蓝图带 revision 号;调性/速度/拍号/时长改过后重建,旧声部标注"按旧蓝图写的",不自动重写已有声部(省 token)。
  • 并发防抖:按项目加 asyncio.Lock,防止用户连点两件乐器时第二个请求把蓝图覆盖。

第二步:分谱 —— LLM 为每件乐器写音符

每件乐器调一次。这是生成 MIDI 的核心步骤。

System 提示词要求只输出一个 JSON:

{"notes": [[小节, 拍, 时值, 音高, 力度], ...]}

字段全部是数字:小节从 1 开始不超总小节数;拍从 1 开始、0.25 的整数倍、小于每小节拍数+1;时值以拍为单位最小 0.25 允许跨小节;音高为 MIDI 音高必须在给定音域内;力度 1–127。

硬性要求(写进 system prompt):

  1. 严格使用给定调性与每小节和弦,强拍用和弦音,经过音只放弱拍
  2. 不写超出音域的音
  3. 同时发声的音不超过给定上限
  4. 尊重"参与的小节",没列出的小节一个音都不写
  5. 与已有声部配合:不抢同一音区,节奏互补而非齐奏
  6. 乐句要有呼吸,不要从头到尾密不透风

User 提示词拼入:

  • 乐器名与角色(melody/harmony/bass/rhythm)
  • 音域;鼓组则给 MIDI 第 10 通道的合法鼓件表(音高数字代表鼓件,只能用它枚举的键)
  • 同时发声上限(按角色给:rhythm 或 harmony 等)
  • 调性 / 拍号 / BPM / 每小节拍数 / 总小节数 / 风格
  • 要写音符的小节(由构型 participation 过滤)
  • 每小节和弦表(第N小节: 和弦 逐行)
  • 已有声部压缩视图(见下)
  • 随机种子:"同样的输入换个种子应当写出不同但同样合理的一版"

已有声部的压缩视图:旋律声部给全量音符(新声部要跟着它走,且数量上限约 220 个才全量);其余声部只给 每小节: 音符数/音区min-max 的摘要。不压缩 token 直接爆。

第三步:残稿抢救(关键差异点)

用 chat_text 而非 chat_json:

  • chat_json 解析失败就整份丢弃重试。音符表动辄上千数字,最常被 max_tokens 截断,重来既慢又费额度。
  • chat_text 返回原始文本、不做自动重试,让代码有机会从残稿里抢救。
  • max_tokens 必须显式放大(如 12000)并显式发送。各家兼容层默认上限差很远,Anthropic 兼容端点强制要求该字段。

残稿抢救算法(不要求整段是合法 JSON):

  1. 先 json.loads 整段试;支持剥掉 ```json 围栏。
  2. 失败则用正则逐个匹配完整的 [小节,拍,时值,音高,力度] 数组(匹配 4~5 个数字的数组,力度缺省给 80)。
  3. 截断前的音符照用,只标记"输出被截断,已保留截断前的音符"。少最后几小节比丢整份强,缺的部分可"重新生成"再补。
_NOTE_RE = re.compile(
    r"\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*,"
    r"\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)"
    r"(?:\s*,\s*(-?\d+(?:\.\d+)?))?\s*\]"
)

第四步:确定性校验修复

LLM 输出不直接落盘,先过 validate_and_repair_part(修复而非报错):

  • 格式不对的音符跳过;小节号超界丢弃;拍位 <1 拉回小节开头、超出每小节拍数丢弃
  • 非法鼓件丢弃;音高超出音域 → 整音八度移位,仍不合则丢弃
  • 同音高音符重叠 → 前一个截短
  • 复音数超限 → 按优先级裁剪
  • 一件乐器一个音符都没剩 → part_empty 告警
  • 每处修复记入 warnings({code, message, count} 形状),前端展示"X 处被修正"

第五步:渲染、落盘与导出

  • 渲染:renderer.render_part(part, blueprint) → 每件乐器一个 WAV。渲染器可选外部合成器(FluidSynth,混响好)或纯 Python 采样播放器(零外部依赖)。写谱模式用固定增益不归一化,保住配器平衡;各声部采样数严格一致;尾音叠回开头做无缝循环。
  • 落盘:谱子 JSON 存 scores/{take_id}.json;take 参数记录 composer、降级原因、note_count、repairs、beat_grid(谱子本身就是拍网格真值:downbeat 精确落在 0 秒,教学评分直接读它,不需要起始点检测)。
  • 局部重绘:只重写某个时间区间的音符(按小节边界向外吸附,避免半小节接缝和声对不上),其余小节原样保留。符号模式下这是真能做的,不需要模型支持音频重绘。
  • MIDI 导出:全部声部合成一个 MIDI(手写 SMF type-1:头块 + 音轨块,含 tempo、time signature、program change、note on/off、controller 等事件),供 DAW / MuseScore 使用。

降级策略(贯穿全程)

步骤失败处理
蓝图(和声)退回规则和声表,只记 fallback_reason
分谱(音符)退回规则作曲,source="algorithmic" + fallback_reason
分谱输出无音符同上
分谱输出被截断保留截断前音符,标记截断

降级事实写进结果元数据(composer、fallback_reason),前端能显示"这一条是本地作曲写的",不会假装是模型写的。

提供方接入(BYOK 通路)

LLM 走 OpenAI 兼容层 + 自定义 base_url(DeepSeek / 智谱 / Kimi / OpenRouter / Ollama 等均有兼容端点),用 httpx 手写单个 POST /chat/completions,不引 SDK。安全要点:key 只存后端(不落前端/localStorage)、base_url 白名单精确主机匹配、限流、任何接口不回显明文 key。

常见陷阱

  • 被截断就当失败 → 必须走残稿抢救,别整份重试。
  • chat_json 用于大输出 → 解析失败丢整份,且部分 provider 忽略 response_format(400 时去掉该字段重试一次)。
  • 上下文不压缩 → 多乐器时 token 爆炸,旋律全量、其余摘要。
  • 并发建蓝图 → 按项目加锁,防止覆盖。
  • 强拍/弱拍不写进提示词 → 模型会写出无层次感的齐奏;明确"强拍和弦音、经过音弱拍"。
  • 音域/鼓件不给 → 模型必然越界,靠后置修复也只能救一部分,先在提示词里钉死。

边界 / Boundaries

  • 不假装降级结果是模型写的。规则作曲兜底的那一条,composer 和 fallback_reason 必须如实写进元数据并在界面上展示。这是这套设计里最不能让步的一条。
  • 不扒现成曲子。提示词里不得要求模型复现某首受版权保护的作品的旋律或和声进行。风格描述说的是风格,不是某一首歌。
  • 不把 key 交给前端。BYOK 的密钥只存后端,不落 localStorage,任何接口不回显明文,base_url 走精确主机白名单。
  • 不用校验修复掩盖提示词问题。音域和鼓件先在提示词里钉死,后置修复是兜底,不是主要手段。修复次数居高不下说明提示词该改了。
  • 不跳过 warnings。修了什么要能查,{code, message, count} 如实记录并展示。

参考实现

MaestrWave 的 M7 写谱演奏模式:

  • composer.py:LLMComposer(blueprint + compose_part)、_SYSTEM 提示词、_salvage_notes 残稿抢救、_digest_existing 压缩视图、AlgorithmicComposer 降级
  • score_gen.py:编排层(ensure_blueprint 并发锁、generate_instrument_score、repaint_instrument_score、project_midi)
  • score.py:乐谱结构、和弦解析、validate_and_repair_part 确定性修复
  • midi_out.py:手写 SMF type-1 导出
  • render.py:FluidSynth 与纯 Python 两个渲染器
  • llm.py:chat_text / chat_json、BYOK 通路(白名单/限流/密钥管理)

变更记录 / Changelog

版本日期变更类型
0.1.02026-08-12从 MaestrWave M7 写谱演奏模式归档进仓库,补触发场景与边界minor

レビュー

まだレビューはありません。使ってみた感想をお寄せください。

同じリポジトリのスキル

概要と使いどころ

当学生说"Common App 活动栏写不下"、"这段活动描述超字数了"、"帮我把这几条压到限制内"时使用。先按官方申请表核对当季字符限额,再逐条检查;一次把所有条目量完并直接给出压缩版,只在需要砍掉实质内容时才用选择题问该保哪部分。每改一版都用工具重新数一遍字符,绝不凭感觉说"这样应该够了"。只压缩,不代写空白。

日本語の概要は準備中です。原文の説明を表示しています。

ChromiteCr/Skills42026年10月16日 更新

当学生说"我参加过一些活动但没整理过"、"帮我把经历录进去"、"我的活动档案是空的"时使用。让学生一口气把记得的活动说出来,直接把这段话拆成结构化的长期事项(角色、组织、起止时间、做了什么、结果、级别),说不清的字段留空标"待补"而不是编,再用一张选择题卡把级别判定与是否重复这类关键项一次问完,然后提案入库。只记录学生真正说出口的内容,级别按事实判定,绝不往高了写。

日本語の概要は準備中です。原文の説明を表示しています。

ChromiteCr/Skills42026年10月16日 更新

当学生问"招生官会怎么看我的档案"、"我的背景还差什么"、"帮我评估一下现在的申请竞争力"时使用。以顶尖大学招生官的视角通读学生的档案、长期活动与已写下的反思,不问任何前置问题直接产出一份指名道姓的坦诚点评:这份材料的亮点、招生官真实的解读方式、明显的短板与空白、以及接下来最值得做的一到两件事。只读不写,不修改学生的任何数据,也不代写任何申请材料。

日本語の概要は準備中です。原文の説明を表示しています。

ChromiteCr/Skills42026年10月16日 更新

当使用者说"帮我填词"、"按这个字数模板写歌词"、"用多Agent对抗式写词"、"这首副歌帮我想几句"时使用,前提是字数骨架和这首歌写什么都已经有了;只有主题、还没定写什么时先用 lyric-concept-builder。拿一份字数骨架和意象系统,派三个 sonnet 子代理按不同路子并行独立起草(场景派 / 口语派 / 意象派),再派一个红队子代理逐句挑毛病,主 Agent 按句裁决拼稿,最后统一辙口与意象。收敛判据卡死:字数用 lyric-doctor 的脚本对模板数、每段同辙、hook 不含陈词、主歌二对主歌一有推进。不抄现有歌词,不替使用者决定这首歌要说什么。

日本語の概要は準備中です。原文の説明を表示しています。

ChromiteCr/Skills42026年10月16日 更新

当想知道一段 AI 回答"哪些能直接用、哪些必须先验证、哪些只能当建议"时使用。按声明类型分四级——低风险草稿材料、观点与策略建议、可查证的事实声明、会改变状态的可执行指令——每级配一句默认处置动作;再按影响半径排序,给出最小的下一步检查。可执行类一律标"不能直接执行;先做代码审阅/沙箱测试/回滚准备",其中破坏性或提权的命令排在最前。教的是一套可复用的复查习惯,不是替使用者把所有内容都核实一遍:本 skill 只分级、排序、定处置,要逐条核实走 ai-output-fact-checker,整段代码要做入库体检走 ai-code-onboarding-checklist。

日本語の概要は準備中です。原文の説明を表示しています。

ChromiteCr/Skills42026年10月16日 更新

当拿到一段 AI 生成的代码,还没决定要不要信、要不要跑、要不要往上继续加东西时使用。先做一遍客观体检:行数与嵌套深度、有没有测试、有没有硬编码密钥或遗留 TODO、依赖是不是真实存在、有没有危险调用(eval/exec、shell=True、递归删除、反序列化、提权、外发网络,按正文给的 grep 模式逐类查);再给风险评级和下一步动作。老实写明它查不了算法正确性——体检通过不等于代码是对的。回答里文字、事实和代码混在一起时先用 ai-answer-triage 分级;只审相对已有代码的一份改动用 ai-diff-review-protocol。

日本語の概要は準備中です。原文の説明を表示しています。

ChromiteCr/Skills42026年10月16日 更新

ChromiteCr のスキルをすべて見る

このスキルの問題を報告する