ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

DeepSeek+Python实战:从提示词工程到MIDI原创音乐生成

DeepSeek+Python实战:从提示词工程到MIDI原创音乐生成 简介面向对AI音乐创作感兴趣的技术开发人员的实战文档。内容整合DeepSeek语言模型与MIDI数字音乐协议从AI作曲技术背景、DeepSeek模型特点与调用方法、MIDI文件解析与预处理讲起覆盖基于DeepSeek的MIDI数据清洗、特征提取、文本化表示再到作曲模型构建训练、音乐参数推理、MIDI文件保存与完整代码示例。值得一提的是文中从数据准备、模型搭建到调优评估展示了一整套工程化流程并给出生成效果评估指标、优化策略以及游戏配乐、影视配乐、个性化音乐推荐等应用案例能够帮助读者系统掌握从零实现AI原创音乐的思路。资源为26页完整PDF文档压缩包仅含1个PDF文件整体大小约1.97MB目录结构清晰文字、图表均正常显示便于逐章阅读实操。目前该文档已有378人学习浏览适合有一定编程基础、希望快速上手DeepSeek音乐生成实战的中高级开发者下载研读。1. AI作曲实战到底在做什么DeepSeek 不产波形它产“乐谱”看到《AI作曲实战DeepSeekMIDI生成原创音乐》这个标题第一反应很容易是“DeepSeek 会不会直接输出一段 wav 音频”。实际跑过一圈就知道DeepSeek 这类大模型最擅长的是把音乐表述成结构化的 MIDI 事件或乐谱文本而不是波形。这个方向解决的是独立开发者和音乐爱好者的一个真实痛点不会乐理、不想买素材版权、又希望批量得到可编辑的原创 MIDI。它适合有 Python 基础、愿意折腾 prompt 的人。接下来我会按最小闭环、乐理约束、本地部署、常见翻车和进阶变奏的顺序把这条路线完整拆给你。2. 跑通最小闭环DeepSeek API 生成 JSON 音符再用 Python 写成 MIDI2.1 为什么先走 MIDI 而不是直接生成音频做 AI 作曲之前最需要立住的不是模型而是输出格式。直接让大模型生成音频常见做法是用扩散模型但它是个黑匣子你无法精确指定每小节的和弦不好修改某一个音实时交互的延迟也很难受。MIDI 不一样它记录的是“在哪个时间点按下哪个音、力度多少、持续多久”体积可以小到几 KB任何一个 DAW 都能打开并逐音编辑。DeepSeek 这类语言模型本质上是离散 token 的概率预测器而 MIDI 事件恰好是高度离散的音高、时值、通道、力度都是整数。让模型直接输出人类可读的 JSON 事件序列再通过 Python 转成 .mid 文件是这条路线里最稳的落地方式。它把“作曲”这个模糊任务变成了“生成结构化文本”任务既方便程序校验也方便人工介入。我一般会把第一步做成“4 到 8 小节单旋律闭环”不急着编曲。这样能最快暴露模型是否真的理解你给的时值单位、音域和小节长度。等你跑通这个最小闭环再谈和弦、多轨和风格控制。2.2 DeepSeek API 调用生成一段 8 小节旋律的最短脚本DeepSeek API 的调用方式与 OpenAI 兼容所以直接用openaiPython SDK 就能连上不需要再造轮子。关键配置是base_url指向 DeepSeek 的地址api_key从环境变量读取。下面是生成 8 小节 C 大调旋律的最短脚本import os import json from openai import OpenAI client OpenAI( api_keyos.getenv(DEEPSEEK_API_KEY), # 不要硬编码密钥 base_urlhttps://api.deepseek.com ) prompt 生成一段8小节的C大调钢琴旋律4/4拍BPM 120。 输出为JSON数组不要输出任何其他文字数组每个元素是一个音符事件 {pos: 0, on: 60, vel: 80, dur: 480} 要求 1. pos是音符起始tick从0开始第1小节起点是0每小节是1920个tick 2. on是MIDI音符号60是中央C范围限制在60到72之间 3. dur是时长480代表四分音符1920代表全音符 4. 旋律要有起伏包含经过音和附点节奏避免连续超过3个相同音。 只输出JSON。 resp client.chat.completions.create( modeldeepseek-chat, messages[{role: user, content: prompt}], temperature0.8, max_tokens2000, top_p0.9 ) content resp.choices[0].message.content print(content)这段代码里最重要的是把“时间单位”写死。pos和dur都用 tick 表示480 tick 等于一个四分音符1920 tick 等于一小节4/4 拍下 4×480。这样模型不需要理解“秒”和“BPM”只做整数运算出错率会低很多。参数方面temperature0.8能让旋律有一定变化但不会完全乱来max_tokens不能给太小8 小节的 JSON 事件大概会有 1500 到 2500 个 token如果只给 500 很容易截断。top_p0.9负责在采样时砍掉尾部低概率 token对稳定音高有帮助。2.3 把 JSON 转成 .midmido 的批处理脚本拿到 JSON 后下一步是转成标准 MIDI 文件。这里我用mido因为它足够轻量不需要像music21那样加载一堆乐理解析器。转换的核心难点是 MIDI 消息里的time字段是相对时间也就是和上一条消息的间隔而不是绝对位置所以要先按绝对时间排序再换算成 delta。import json import mido from mido import MidiFile, MidiTrack, Message # content 来自上一步的 API 返回值清理 markdown 代码块 content content.strip() if content.startswith(): content content.split(\n, 1)[1] content content.rsplit(, 1)[0] notes json.loads(content) mid MidiFile(ticks_per_beat480) track MidiTrack() mid.tracks.append(track) # 0 号音色是钢琴 track.append(Message(program_change, program0, time0)) # 把每个音符拆成 note_on 和 note_off记录绝对时间 events [] for n in notes: events.append((n[pos], note_on, n[on], n[vel])) events.append((n[pos] n[dur], note_off, n[on], 0)) events.sort(keylambda e: e[0]) cursor 0 for abs_time, kind, note, vel in events: delta max(0, abs_time - cursor) cursor abs_time track.append(Message(kind, notenote, velocityvel, timedelta)) mid.save(deepseek_melody.mid) print(saved, len(notes), note events)逻辑说明我将每个音符拆成一个note_on和一个延迟了dur的note_off然后按绝对时间排进events。循环里cursor记录上一条消息的绝对时间delta是两者差值这样即使两个音符同时开始也不会互相覆盖时间轴。提示ticks_per_beat480必须和 prompt 中的 480 一致否则导出的 MIDI 会被 DAW 按错误的时值播放。如果发现 notes 里混入了channel、pos缺失等字段解析时需要做校验可以在json.loads之后加一个列表推导只保留必要字段。不要把这个转换脚本做成一次性的后面多轨生成还要继续用它只是加一个按 channel 分组的逻辑。2.4 第一次验证听声音、看钢琴卷帘、改参数转出来的文件先别急着交给别人先自己验证。最常见的验证方法是打开 DAW例如 Reaper、Cubase 或 LMMS把 .mid 拖进去看钢琴卷帘有软音源的话直接播放。没有 DAW 时可以用命令行合成器fluidsynth -a alsa /usr/share/sounds/sf2/FluidR3_GM.sf2 deepseek_melody.mid重点看三件事。第一音符是不是落在网格上。如果大量音符错开几十个 tick说明模型对pos的小节换算理解不准。第二旋律音域是否在你限定的 60 到 72 内跨度过大会让听感突兀。第三整体节奏和 prompt 里的 BPM 是否匹配。跑完这个最小闭环你已经完成“DeepSeek MIDI”的链路。这里最容易踩的坑是 prompt 里给了太多要求比如既要求附点又要求分解和弦模型会顾此失彼。我的习惯是第一版只保留音域、小节、时值单位三个约束成功之后再逐步加乐理要求。3. 让 DeepSeek 懂乐理提示词里的调式、和弦进行与节奏控制3.1 把乐理约束写进 promptC 大调与和弦进行的共同约定最小闭环能响之后你会发现 DeepSeek 生成的旋律还是“幼稚”没有明确调性、和弦进行像是随机游走。这时候就要在 prompt 里加入显式的乐理约定。常见的做法是四项约束调式、拍号、和弦进行、节奏密度。调式直接写“C 大调”还不够最好再给一组可用和弦比如“只使用 C、G、Am、F 四个和弦”。和弦进行要按小节给例如“第 1 小节 C第 2 小节 G第 3 小节 Am第 4 小节 F”。节奏密度用每小节的音符数量描述例如“前四小节平均每小节 4 到 6 个音符后四小节允许八分音符密集跑动”。我把这些字段做成一个 prompt 模板请用 C 大调写一段 8 小节钢琴独奏拍号 4/4。 和弦进行C - G - Am - F - C - G - F - C。 每小节 1920 tick。 音域中央 C60到高音 G67。 节奏密度平均每小节 5~8 个音符避免全十六分音符。 输出 JSON 事件数组字段包括 pos、on、vel、dur。注意我没有写“要原创”因为模型本身就是在采样概率分布只要你不让它模仿某首具体歌曲它产出的不可能和已有 MIDI 文件完全一致。写“要原创”反而会让模型在歌词或文字层面做无用功。3.2 用函数自动拼 prompt一次生成主旋律与伴奏轨如果每次都手写这么长的 prompt很快就会烦。更好的方式是把配置和 prompt 生成逻辑封装成函数这样以后要换调式、换和弦、换风格只改参数即可。def build_compose_prompt( keyC, progressionC - G - Am - F - C - G - F - C, bars8, stylepop ballad, tempo90 ): return f请用{key}大调写一段{bars}小节的{style}钢琴曲拍号4/4BPM {tempo}。 和弦进行{progression}。 每小节1920 tick480 tick 四分音符。 输出JSON事件数组每个事件包含 channel: 0代表右手主旋律1代表左手伴奏 pos: 起始tick on: MIDI音符号 vel: 力度 dur: 时长tick 要求右手音域60-72左手音域36-60每小节总时长必须等于1920。 只输出JSON。调用的时候只需要传和弦进行和风格返回的 prompt 直接塞给前面写好的 API 请求。生成的 JSON 里会带channel转 MIDI 时就可以把它拆成两条音轨。因为左手伴奏通常包含多个音符同时发声我用mido做多轨时会在events里按channel分组再分别建MidiTrack。这里有一个血泪经验别一上来就让模型同时生成旋律、伴奏、贝斯三轨。模型会在前几小节做得不错到第 7、第 8 小节开始忘记你给的音域约束低音突然蹦到一个离谱的音高。我一般是先让单轨跑稳再加入第二轨给模型喂“上一个轨道的完整 JSON”作为参考再让它补伴奏轨。3.3 采样参数怎么调temperature、top_p 对原创性的影响在 DeepSeek 这类模型里旋律音符生成就是一个个 token 采样。temperature控制概率分布的锐利程度top_p控制采样候选集大小这两个参数直接决定你听到的是“保守的爬音阶”还是“失控的噪音”。下面是我在 MIDI 生成任务上常用的参数参考参数建议范围对 MIDI 生成的影响temperature0.6 ~ 0.9低于 0.5 时旋律很稳定但容易变成音阶练习高于 1.0 时经常出现不和谐跨跳top_p0.85 ~ 0.95值越大随机性越强0.99 会偶尔冒出离谱的低音或超高音max_tokens2000 ~ 40008 小节单轨至少 2000多轨建议 4000frequency_penalty0 ~ 0.3能减少连续相同音但太大会让旋律失去重复和对称感presence_penalty0 ~ 0.2对已经出现过的音高做轻微惩罚适合避免整曲只重复几个音不同模型版本对温度的感受不完全一样这就是采样参数里的“玄学”。我的做法是固定top_p0.9然后只动temperature先 0.6 跑三遍再 0.85 跑三遍听一组再把最顺耳的留下来。如果一次生成的旋律音符跳跃太远不要继续调 prompt 折腾把top_p从 0.95 降到 0.9往往比改和弦进行更直接。4. 把生成搬到本地vLLM 部署 DeepSeek 做离线作曲4.1 本地部署解决什么问题批量、隐私、可控接口API 模式适合试错但真把 AI 作曲放进内容批量生产流程你很快会遇到三个问题并发请求被限速、旋律数据不想传到别人服务器、每次请求的 token 成本随曲目长度线性上涨。本地部署 DeepSeek 模型配合 vLLM 提供 OpenAI 兼容接口是解决这三个问题的常见做法。本地部署不是小白起步适合已经跑通 API 闭环、手里有至少一张 16G 以上显存显卡的人。模型尺寸选 7B 左右最舒服太小的 1.5B 模型写 MIDI 逻辑经常断太大的 70B 在消费级硬件上跑不起来。我的建议是先用 API 确定 prompt 和转换链路再切本地模型避免同时排两个黑匣子的错。4.2 vLLM 启动 OpenAI 兼容服务的最小命令与参数含义vLLM 是目前本地推理里部署体验最顺手的框架之一启动后直接暴露一个/v1接口可以用和 DeepSeek API 几乎一样的代码调用。常见的启动命令长这样vllm serve ./models/deepseek-distill-7b \ --port 8000 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9 \ --trust-remote-code \ --served-model-name deepseek-local参数含义./models/deepseek-distill-7b是模型所在目录换成你实际下载好的路径--max-model-len 8192表示支持最长 8192 个 tokenMIDI 生成的多轨 JSON 很容易超过 4096所以起步给到 8192--gpu-memory-utilization 0.9允许 vLLM 用掉 90% 显存留一点给系统和显示--trust-remote-code是很多开源模型的配置代码需要执行不加会直接拒绝加载--served-model-name是给本地模型起的名字方便后续客户端识别。启动日志里看到Uvicorn running on http://localhost:8000就说明服务起来了。为了提高吞吐可以加--max-num-seqs 8让多个生成请求并行但如果单请求的max_tokens已经开到 4000并行数太高容易把显存打爆。4.3 把 API 调用脚本切换到本地服务切换成本很低只要改OpenAI客户端的base_url和api_key。vLLM 不校验 key随便写一个占位符就行from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, # 本地 vLLM 服务 api_keyEMPTY ) # prompt 用第 3 章的 build_compose_prompt 生成 resp client.chat.completions.create( modeldeepseek-local, # 要和启动时的 served-model-name 一致 messages[{role: user, content: prompt}], temperature0.8, max_tokens4000, )这里最容易翻车的是model名不一致。启动时写了--served-model-name deepseek-local客户端就必须传同样的名字如果漏了这一项vLLM 会报 404。另外本地模型的风格和 API 不一样同样温度 0.8本地 7B 模型生成的旋律可能更“直白”别用 API 的参数表硬套。4.4 量化与上下文窗口本地作曲的边界为了在消费级显卡上跑大多数本地部署会选 AWQ 或 GPTQ 量化版本。量化对 MIDI 生成的影响比通常文本生成更微妙MIDI 事件里大量数字是 48、60、480 这种关键 token量化误差一般不会让数字本身崩坏但会让模型在长序列里更容易遗忘前面小节的和弦导致后段跑调。上下文窗口也是边界。一篇 8 小节单轨 JSON 大约 2000 token但如果要做 32 小节、三轨编曲prompt 加输出很轻松超过 8000 token。我一般会把长曲子切成 8 小节一段每段生成后保存再让第二个 prompt 做衔接而不是一次性让模型生成 32 小节。这样即便本地模型上下文只有 8K也不会用到一半开始胡写。5. AI作曲实战中的五个常见坑现象、原因、解决5.1 输出被截断JSON 解析一半就崩现象API 或本地模型返回的内容在数组中途戛然而止json.loads直接报错。原因最常见是max_tokens给少了或者模型在输出末尾加了说明文字把 token 预算浪费掉。解决把max_tokens调到 4000prompt 里明确“只输出 JSON”并且在解析前先清理 markdown 代码块。可以写一个宽容解析函数抓到json.JSONDecodeError时去掉最后不完整对象再试一次。5.2 生成的旋律全是“音阶练习”没有记忆点现象单独听每个音都对但整体平平无奇像在爬音阶。原因temperature太低模型在你限定的音域里只敢选概率最高的相邻音同时 prompt 里没有“乐句”概念它不知道需要重复和变化。解决把温度提到 0.8并在 prompt 里加一句“第 1 至第 2 小节建立动机第 3 至第 4 小节做变化重复”。让模型理解曲式比单纯加“要好听”有效得多。5.3 和弦轨和旋律轨叠在一起听感像一团浆糊现象把多轨 JSON 转成 MIDI 后所有音符挤在同一时间点低音、旋律、装饰音全混在一起。原因模型没有严格遵守channel分工或者你在转换时把所有通道的note_on都塞进了一条MidiTrack。解决在 prompt 里明确“channel 0 和 channel 1 不能互换”转换脚本里按channel分组。导入 DAW 后先静音一轨确认分轨正确再处理音色。还有更稳的办法生成完成后检查每个channel事件的pos分布如果发现某一轨整小节都是空白说明模型把该轨漏了。5.4 小节总时长对不上时值“越走越快”现象导入 DAW 后后半拍越来越乱钢琴卷帘里每小节长度不一。原因模型对 1920 tick 的算术并不精确有时少写几个事件有时多写了几百 tick。解决后处理里按小节做“对齐”和“裁剪”。用pos // 1920判断小节号把每小节总时值修正为固定值如果超过 1920就把最后几个事件的时间压缩到合理范围。这个后处理函数我一般会单独写一个模块而不是每次都手工改。5.5 本地模型的“音乐能力”明显比 API 弱现象同一份 promptAPI 生成的旋律能听本地 7B 量化模型生成的却像乱敲键盘。原因本地模型参数量小且 MIDI 事件序列不是它训练数据里最占主导的部分量化会进一步削弱长距离依赖。解决换用蒸馏了推理能力的模型比如 DeepSeek-R1 蒸馏系列的 7B/14B生成前在 prompt 里加一句“先写和弦进行再写旋律最后输出事件”把推理链拆出来。如果还不行就只把本地模型用于实验正式出片回退 API。6. 进阶技巧把已有 MIDI 变成 few-shot 样例指挥 DeepSeek 改风格6.1 先把 MIDI 序列化成文本再塞进 prompt当你对某个 MIDI 片段有偏好时与其用文字描述“想要爵士风”不如直接把参考 MIDI 的前 4 小节转成 JSON 事件贴进 prompt。DeepSeek 看到具体事件后模仿和变奏的稳定性会明显提高。序列化代码不复杂读取每个note_on和note_off按绝对时间整理成{pos, on, dur, vel}。def midi_to_note_events(path): mid MidiFile(path) notes [] for track in mid.tracks: t 0 for msg in track: t msg.time if msg.type note_on and msg.velocity 0: notes.append({pos: t, on: msg.note, vel: msg.velocity}) elif msg.type note_off: for n in reversed(notes): if n[on] msg.note and n.get(dur) is None: n[dur] t - n[pos] break return notes[:16] # 只要前 16 个事件这段代码把 MIDI 转成绝对时间事件并给每个note_on补上dur。注意 MIDI 的 note_off 可能被表达成 velocity 为 0 的 note_on如果你的参考文件是这样的需要在判断里也把这种消息当作结束。6.2 让模型做主题变奏的迭代流程把参考事件放在 prompt 开头要求“保持和弦框架改变旋律装饰音”再把输出转成 MIDI。我会同时生成三个版本导出后放进同一工程对比最后选一个再丢回模型做“把第 3 小节的节奏型复制到第 7 小节”这样的局部修改。这个方法比反复调温度更可控。现在我做 AI 作曲拿到一个新模型的第一件事就是拿一段已经听过无数遍的 MIDI 做 few-shot 变奏而不是从 prompt 从零生成。先确认它的模仿能力再测它的变奏边界因为变奏及格才意味着原创不跑调。这个习惯帮我省掉了很多调 prompt 的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表