Skip to content
Back to skills

Llm Midi Composition

ASecurity

当使用者说"让 AI 写谱不要直接生成音频"、"做一个 AI 作曲的后端"、"LLM 输出 MIDI 怎么做"、"生成的音符老是越界/被截断"、"要能导出 .mid 给 DAW"时使用。用 LLM(BYOK / OpenAI 兼容接口)从零生成可用的管弦乐 MIDI 与分轨音频:两级调用(整曲和声蓝图 + 逐乐器音符表)、提示词结构、被 max_tokens 截断时的残稿抢救、确定性校验修复、渲染与 MIDI 导出,以及全程的规则作曲降级策略。降级事实必须如实写进元数据,不假装是模型写的。参考实现是 MaestrWave 的 M7 写谱演奏模式。

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 7, 2026
ai-agentspythongo

Works with

  • cursor

Security analysis

A100/100

Scanned November 7, 2026

npx -y skills add ChromiteCr/Skills --skill llm-midi-composition --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Llm Midi Composition?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Llm Midi Composition
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/chromitecr-llm-midi-composition/badge)](https://www.skillsdirectory.com/skills/chromitecr-llm-midi-composition)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: llm-midi-composition
description: 当使用者说"让 AI 写谱不要直接生成音频"、"做一个 AI 作曲的后端"、"LLM 输出 MIDI 怎么做"、"生成的音符老是越界/被截断"、"要能导出 .mid 给 DAW"时使用。用 LLM(BYOK / OpenAI 兼容接口)从零生成可用的管弦乐 MIDI 与分轨音频:两级调用(整曲和声蓝图 + 逐乐器音符表)、提示词结构、被 max_tokens 截断时的残稿抢救、确定性校验修复、渲染与 MIDI 导出,以及全程的规则作曲降级策略。降级事实必须如实写进元数据,不假装是模型写的。参考实现是 MaestrWave 的 M7 写谱演奏模式。
category: songwriting/composition
version: 0.1.0
status: draft
priority: P1
compatible_agents:
  - claude-code
  - cursor
  - codebuddy
  - generic-llm-agent
display_name: 让 LLM 写谱
outputs:
  - chat
max_rounds: 30
suggest_hint: 想让 AI 写谱而不是直接产音频?用「让 LLM 写谱」搭一条能容错的生成链路
---

# 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:

```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. 截断前的音符照用,只标记"输出被截断,已保留截断前的音符"。少最后几小节比丢整份强,缺的部分可"重新生成"再补。

```python
_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.0 | 2026-08-12 | 从 MaestrWave M7 写谱演奏模式归档进仓库,补触发场景与边界 | minor |

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.