音频分析 · 现状与结论
给 DSH 加本地音乐理解能力的实验项目。 目标:让文本大模型能回答关于音乐的问题,不是做 FL 参数推荐器,不是播放器。 更新:2026-10-19
这份是开发说明,不是安装指南。 只想装上用,看同目录的《安装说明.md》。
⚡ 交接速览(新会话先看这段)
现在有什么:一个 DSH 插件,注册了三个工具 + 两个命令
| 工具 | 作用 |
|---|---|
audio_analyze |
分析整首音频,返回文本或 JSON(可 brief 压缩) |
audio_section |
只分析一段 [start, end],回答"2:40 那里怎么回事" |
audio_provision |
装引擎(建 venv / 装依赖 / 下 YAMNet);已有 .provisioned 标记时直接返回"已装" |
| 命令 | 作用 |
|---|---|
/audio-status |
看引擎状态(Python 环境 / 脚本 / 模型 / 安装标记) |
/audio-install |
一键装引擎,返回装了什么、走了哪个镜像 |
安装策略:分析时不会偷偷装依赖(用户得先知道要下 470 MB)。引擎缺失时工具返回
ENGINE_NOT_INSTALLED,让用户跑 /audio-install,或者明确让助手代装。
插件位置:dsh-soundcheck-plugin\(已 dsh plugin add 到 desktop profile)
**引擎位置**:开发机用项目根目录(venv + analyze.py);分发包用插件自带的 engine\
分发包:dsh-soundcheck-0.5.1.zip(16.5 MB),由 pack.py 生成
GitHub:ALittleLov/dsh-soundcheck —— 旧账号 dalekotsvet 已被封(2026-10-20),
仓库已迁移过去,本地 remote 已切,旧下载链接全部失效
性能:4 分钟 mp3 整首约 11 秒(此前 66 秒)。两处白干的活已去掉: chroma 原本为调性和和弦各算一遍 CQT;YAMNet 原本把 mp3 再解码一遍。
输出长什么样(文本模式,默认只给判断):
[SCOPE] 只有片段分析时才出现:分析了哪一段
[判断] 一段人能直接读的结论:时长/速度层级/调性/母带/结构重心/最大反差/收尾
[依据] 判断背后的那几个数字(峰值、RMS、起音、调性、拍速)
[可信] beat / 调性 / 根音 的适用性分数(0-1)
[听不到] 这类分析死也听不出来的东西
--detail(或工具参数 detail: true)才输出完整明细:
[FIT] 适用性自评 | [META][BPM][KEY][DYN] | [ACT] 活动度 | [FORM] 能量走势
[CHORD] 根音骨架 | [SEG] 段落边界 | [SRC] 声源 | [说人话] 旧版散文式总结
关键设计:默认先给结论,数字只在 [依据] 里。定性来自"别给用户甩一堆数据"这条反馈。
不给流派判断 —— 流派是听出来的,不是测出来的;要的话给"方向 + 置信",并标成推断。
当前瓶颈:说不出"音乐内容"(乐器、人声、语言、氛围)。光靠 DSP 补不上——见文末「9 个维度」,9 格里有 2 硬、5 半、2 空。 MusicNN(50 类)只覆盖西方音乐大类,不含中国民乐。
未发版的改动(2026-10-20 晚,等用户点头才发 0.5.3):
analyze.py术语改英文:抽空/砸回→drop X dB/回来;直接收掉→hard cut;淡出→fade out。起因:QQ 接入的用户把「抽空了一截」读成「最满」,方向弄反了 —— 事实证明形容词有歧义,数字不会- 判断里允许单个数字锚点(如「1:36 drop 9 dB」);自检同步放宽为「一行两个以上才报」
- 这三处已在工作区生效,但
repo/engine与plugin/engine尚未同步,发版时一起同步
当前待办(讨论完,未动手):
- 按需 stem 分离(4-stem 里吉他在 other,要 6-stem;CPU 慢需后台)
- 歌词接入(已验证:LRC 优先、ASR 仅兜底候选,见「歌词 ASR 验证」一节)
- 从 MOSS 借能力(当 Teacher 打标 → 训小模型;不把大模型塞进插件)
- 链接入口(取件与读件分离:下载归 ncatbot,插件仍只吃本地文件) 详见文末「下一步候选」与「9 个维度」。
踩过但放弃的路:
- MOSS-Music 8B(18GB + CUDA-only)→ 用户否决
- Essentia Discogs-EffNet(400 类风格,20MB)→ essentia.upf.edu 域名在本机不通,且官方 Windows Python 绑定不支持
- GTZAN 训练(2001 年老数据、10 类、无国潮)
- YAMNet 1024 维 embedding 做音频检索(2026-10-20 实测)→ 探针脚本见
_archive\probe_embed*.py。 用 8 首歌、音量归一化、中位数池化,按曲风分组算"同组−异组"区分度: YAMNet +0.106、MFCC 对照 +0.033。向量确有信息(为 MFCC 的 3 倍), 但最高相似对是 HAYA 蒙古民乐 ↔ 枪花硬摇滚(0.795),排序不符合音乐内容。 结论:向量有微弱信号,不足以支撑"这是马头琴/那是古筝"级别的判断; 要用出它的价值需要一批干净标注的参考样本,工量等于另起项目。不加入主流程。 同类线索(MusicNN 50 类标签、OpenL3 18MB 音乐嵌入)未跑,若将来有标注集再议。 - Silero VAD v5 判断人声(2026-10-20 实测)→ 模型
_archive\silero_vad.onnx(2.2 MB),探针_archive\probe_vad.py。 四首歌的人声块占比: HAYA papillon(有人声)12.0%、枪花(有人声)6.3%、 I Saw(纯器乐)7.1%、大提琴 cover(纯器乐)1.4%。 纯器乐比有人声的还高,阈值无法区分;最长连续人声段只有 1.44 秒,抓不住整段歌声。 结论: VAD 训练在『说话』上,歌声(长音/颤音/伴奏掩蔽)不在分布内 —— 既漏检又误检,不可用于音乐。 它偶尔高置信度误判(max 0.958),与 YAMNet 把唢呐报成 Choir 是同一个病根。
用户真正想要:Gemini 那种描述("马头琴、蒙古语、草原庆典")。已确认只能靠 Gemini API(约 ¥0.13/首),但用户不愿花钱/翻墙。
目前定下的方向:不追求"像 Gemini"的措辞,而是把"能测的"合成为判断、把"测不到的"明说。
目录结构
音频分析\
├── analyze.py 主分析脚本(结构/BPM/调性/根音候选/动态 + 可选 YAMNet)
├── yamnet_test.py YAMNet 单独测试脚本(看时间线用)
├── yamnet.onnx YAMNet 模型,16 MB
├── yamnet_class_map.csv 521 类标签,14 KB
├── dsh-soundcheck-plugin\ 插件本体(源码,不含引擎)
│ ├── lib\index.js 宿主半侧,注册三个工具
│ └── cordis.patch.yml
├── README.发布.md 给用户看的说明(自述 + 安装合并版,打包时改名 README.md)
├── README.md 这份开发说明(不打包)
├── build\dsh-soundcheck\ 打包暂存区(pack.py 生成,已脱敏)
├── pack.py 打包:拼装 + 脱敏扫描 + 压 zip
├── dsh-soundcheck-0.4.0.zip 分发包,13.6 MB(不含 Python 依赖)
├── test-plugin.mjs 插件端到端测试(假 ctx 收集工具,真跑每个工具)
├── test-activity.py 活动度指标的单独测试
├── venv\ Python 虚拟环境(librosa + onnxruntime),468 MB,不发
├── wheels\ 离线 wheel 缓存,118 MB(重装用)
├── install-offline.sh 通用离线装包脚本(curl 下 wheel + pip --no-index)
├── download-wheels.sh 最初的 librosa 依赖批量下载
├── fix-deps.sh 最初的迭代补包
├── out.json 一份完整 JSON 样例
└── out.brief.json 一份 brief JSON 样例(约 530 B)
打包与分享:
- 分发包 = 插件代码 + 引擎(analyze.py 54 KB + 模型 16 MB)= 13.6 MB
- 用户机器的 Python 依赖(468 MB)由
audio_provision在安装时下,不进包 - 包里的引擎在
engine/,用户解压到哪都能自己找到;开发机上 profile 里用config.root指回工作区 - README 分两份:
README.md(这份,开发用,不打包)和README.发布.md(用户版,打包时改名为包内README.md) - 脱敏是硬门槛:
pack.py先复制到build/,替换本机路径,再扫一遍禁用词 (用户名、本机工作目录、歌库文件名,词表就写在pack.py里);扫到就中止打包,不悄悄发出去 - 改完
analyze.py必须重跑pack.py,否则分发包里还是旧的
发版流程(改完要更新 GitHub 时必须走)
推送前必须停下。 代码改完只在本地验证,把结果给用户看;得到明确同意后, 才允许 commit / tag / push / 建 Release。禁止自行判断「这个改动是对的」就直接推。 2026-10-20 定下的规矩:当时 0.4.2 抢先推送,被纠正过一次。
本地 commit 不等于 push,攒着发。 平时随便 git commit(纯本地,零网络流量);
到发版点再一次性 push(main + tag)+ 建 Release。一天 1–2 次 push 就够。
别滑回「改一下就推一下」——旧账号被封的教训之一就是节奏太密(新号 + 代理 IP + 高频操作,
是典型的自动化风险画像)。
不要改动完就 git push,那是"直接改文件"。 每次改动都按版本走:
- 本地验证 → 把结果给用户看 → 等明确同意(这步不能跳)
- 改完、本地验证通过(
analyze.py实跑一次看输出) - 升版本号,两个文件必须一致:
repo/dsh-soundcheck/package.json与dsh-plugin.json- 纯修正 → patch(
0.4.0→0.4.1);用户可见的行为变更 → minor(0.5.0)
- 纯修正 → patch(
- 写
repo/dsh-soundcheck/CHANGELOG.md对应小节,写清"改了什么、为什么" - commit(沿用 conventional commits:
fix:/feat:/docs:/chore:,正文中文) - 打 tag:
git tag -a vX.Y.Z -m "..."(历史上从来没过 tag,从现在起必须有) git push origin main && git push origin vX.Y.Z
配套动作:
- 三份引擎副本要同步:
analyze.py(根) →repo/dsh-soundcheck/engine/→dsh-soundcheck-plugin/engine/。只改根目录那份,发布的包里还是旧的 - 运行版插件的
package.json/dsh-plugin.json版本号也要跟着升,否则装的和发的不一致 - 发版后重跑
pack.py产出新 zip
推送失败的常见原因(本机实测):github.com:443 连不上(网络层),
或凭据管理器弹不出窗(非交互 shell 没有 TTY)。两者都不是代码问题,
在普通终端里手动跑一次 git push 让它弹出登录窗即可。
用法
cd /d/DSH/音频分析
export PYTHONUTF8=1 # 必须!否则中文/方块字符会崩
# 人类可读文本(默认:一段判断 + 依据 + 可信 + 听不到)
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac"
# 完整测量明细(旧的仪表盘:FIT/能量曲线/和弦骨架/声源)
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --detail
# 结构化 JSON(插件调用)
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --json
# 压缩 JSON:只留结论级字段,约 1/25 的体积
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --brief
# 只分析一段(--start/--end 接受秒或 m:ss)
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --start 1:30 --end 2:10
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --start 90 --end 130 --brief
# 分轨(可选组件;首次约 60–90 秒,之后读缓存)
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --stems
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --stems 6s
# 歌词听写(可选组件:先分离人声轨,再 faster-whisper;只输出候选段)
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --asr
./venv/Scripts/python.exe analyze.py "<音乐目录>\music.flac" --asr small
# 只跑 YAMNet 看时间线
./venv/Scripts/python.exe yamnet_test.py "<音乐目录>\music.flac" --start 60 --dur 30
# 插件回归测试(用 DSH 自带的 node)
/node test-plugin.mjs
# 打包分发包(改过 analyze.py 之后必须重跑)
./venv/Scripts/python.exe pack.py
注意:
- 中文路径必须硬编码或加引号传。经 bash 变量传递会编码坏掉。
- 片段分析的时间戳是全曲绝对位置,不是片段内相对时间。
现在能做什么
| 能力 | 状态 | 说明 |
|---|---|---|
判断式输出 [判断] |
✅ 默认形态 | 先给结论,数字只在 [依据];流派不猜 |
| 结构 / 能量走势 | ✅ 可靠 | 16 秒块 RMS,0 dBFS 为满刻度 |
| BPM | ✅ 可靠 | 含倍频校验 + 低频第二意见(但校验不出"检测器本身锁在双倍速") |
| 调性 | ✅ 可用 | Krumhansl 模板 + Top3 候选 + 歧义分类 |
适用性自评 [FIT] |
✅ 最有价值的一块 | 在 --detail 与 JSON 里,决定哪些结论能信 |
| 声源 / 乐器(YAMNet) | ✅ 部分可用 | 16 MB,1000× 实时,CPU;情绪/风格标签已剔除 |
| 响度 / 削波 | ✅ 可靠 | 真峰值 + 整体 RMS + 贴顶点计数 |
| 和弦 | ⚠ 只到根音级 | 根音候选骨架;性质/七度分不清。见结论 4 |
| 片段分析 | ✅ 可用 | audio_section,时间戳为全曲绝对位置 |
| 分轨(人声检测) | ✅ 新增 0.6.0 | --stems 4s/6s:各轨能量占比 + 人声判定;组件可选(约 150 MB),缓存后秒回 |
| 歌词听写 | ⚠ 候选级 | --asr:4s 人声轨 + Whisper;只输出「较确定」段,不是逐字准确;见「歌词 ASR 验证」 |
| 流派 / 风格 | ❌ | 听不出来;工具会明说"风格判断不了",不编 |
| 自然语言描述 | ❌ | 需要大模型(已决定不做) |
四个最重要的结论
1. [FIT] 适用性自评必须保留
它决定"这些结论能信多少"。设计它的时候,自己被它抓出了 10 个 bug。
每个维度都要区分两种失败:
| 失败类型 | 表现 | 例子 |
|---|---|---|
| 体系外(模板不适用) | 绝对可信度低 | 维吾尔音乐 vs 西方调性模板(key_r 0.55) |
| 体系内歧义 | 绝对可信度高、差距小 | 属调混淆(0.91 vs 0.83) |
只看后者,就会把"模板不适用"误判成"可用但有歧义" —— 这是最危险的错。
调性的判据:
key_r(绝对相关度):< 0.65 判定"模板不适用"gap(最佳与次佳差距):≥ 0.15 判定"调性明确"
四首实测:0.913 / 0.900 / 0.550 / 0.894 —— 三首西方体系 + 一首维吾尔,分得很干净。
2. YAMNet 的边界很清楚
| 能给 | 不能给 |
|---|---|
| ✅ 乐器/声源(Percussion / Singing / Drum) | ❌ 情绪(Sad/Tender 是猜的) |
| ✅ 地域线索(Music of Asia / Bollywood) | ❌ 风格(Christmas/Christian 是噪声) |
| ✅ 相对比较(两首像不像) | ❌ 绝对判断(分数只有 0.01–0.05) |
原因:AudioSet 训练数据偏西方流行,没有"抒情/慢板"这类类别,模型找不到就跳到 Christmas/Christian 这种特征相近的类。
用时必须带上这句警告,否则会把"圣诞音乐"写进报告。
2026-10-19 补充:中文乐器这层是噪声。 实测把「唢呐」报进了两首根本没有唢呐的曲子
(一首古筝领奏的器乐曲、一首民乐曲),「二胡」在各首之间到处排第一。
它报的不是乐器,是"中国民乐"的刻板标签。所以判断里已不再出现配器行,原始分数只在 --detail 存档。
同一天还试了 CLAP(零样本,文本比音频):8 首里乐器 top-1 只对 4 个,全是西方通用类 (钢琴 / 电吉他 / 电子鼓);古筝、琵琶、马头琴一个都没进前四;风格则「流行」几乎霸榜。 第一版用中文提示词是无效的(CLAP 文本端是英文训练的),改英文后西方乐器变好、中文乐器仍然为零。 结论:零样本这条路对中文乐器不通,已删除实验环境(2.7 GB)。这也说明早先"Zither→古筝"命中是运气。
2026-10-21 补充:非西方打击乐同样是盲区。 《两只猫头鹰》(代青塔娜,纯吟唱剪辑版)实测, 人耳明确听出的沙锤与鼓,YAMNet 全量 521 类里最高仅 0.09(Rattle)与 0.02(Drum 系),0.10 阈值下零命中; 同一首只有吉他与钵/锣成立(吉他 0.52,集中在 0:01–0:26;钵 0:14、锣 3:08、钵 3:20,均为点状)。 "模型没标签"不等于"音频里没有",这类判断只能靠测量,不能靠模型默不作声。
替代做法(实测可用,暂不入插件):librosa STFT 分频带(6–12k / 2.5–6k / 0.8–2.5k / 250–800 / 80–250 / 30–80 Hz),
每带做包络差分 + scipy.signal.find_peaks 取打击点,统计间隔中位数与变异系数 cv。
判据:cv < 0.5 视为该带节律规律,可判为"节拍器层"。该曲 6–12k 带 cv=0.42、间隔 0.44s、
主周期 0.89s(=67 拍),与 BPM 66 的八分音符吻合 → 沙锤;低频 30–250 Hz 占能量 35%、
打击点密但 cv>1.1 → 打击铺底,不是节拍器。参考脚本 _archive/band_rhythm_probe.py。
3. BPM 会半速误判,而且旧的自校验抓不到
实证:一首自作曲,beat tracker 报 74.9,真实约 150。
为什么原来的"倍频校验"失效:beat_track 的 tempo 和"节拍间隔反推"来自同一次错误判断——它锁在"每两拍算一拍",两个数字当然一致。等于自己给自己作证。
修法:用独立信号做第二意见 —— 30–200Hz 频段 onset 间隔的中位数。同一首歌这样算出 156.6,差 4.7%,并在文本里主动报警。
局限:kick 和 bass 抢同一频段,纯低频检测本身也有天花板(可能把 bass 音符当成 kick)。这是"第二意见",不是"正确答案"。
补充实测(一首钢琴曲):beat tracker 98.7,低频第二意见 216.3。用 onset 自相关独立看, 峰在 194–216 一带(约 2×98.7)—— 说明两个拍层级都真有能量,不是谁算错了。所以措辞已经从 "很可能真速是后者"改成"两个拍层级都有能量,别把其中一个当唯一答案"。钢琴/无鼓曲目里, 低音音符会伪装成底鼓,这条第二意见尤其容易误报。
2026-10-21 补充:倍频错误是双向的,而且合成信号测不出来。
- 合成 click 轨(80/120/160)测得很准(误差 ≤2 拍)—— 但真实音乐里它锁错倍频很常见
- November Rain 官方约 80,检测器报 156.6(约两倍),低频第二意见又给 304
- 用户反馈:一首摇滚报 80,听感 160(约一半)
- 所以「报 156 实际 80」和「报 80 实际 160」都会发生 —— 方向相反,同一病根
修法(已落地):新增 bpm_layers(),永远同时给两个倍频层级,不再只报一个数:
- 判断里:
拍速是两层:157 和 78,两个都可能,取决于你怎么数拍(有分歧时另写「低频层和瞬态层各说各的」) [依据]里:157 / 78 拍(两层)- JSON:
bpm_layers给两层原始值,不标注哪个是「真速」
关键补充:《月之暗面》(网易云权威值 69)检测 139.7(约两倍)——前后半段一致、 三个信号自洽,说明连「节奏稳定的歌」也会锁双倍。所以不再推荐哪层是主答案: 推荐错了比不推荐更糟,只把两层摆出来,由听的人按自己的数拍方式判断。
Beat This 集成(2026-10-21 晚,0.6.6 候选):BPM 检测改为可选组件——
模型 dsh-soundcheck-data/models/beat_this/beat-this-final0.onnx + frontend.json
(82 MB,MIT,ONNX,纯 onnxruntime,不需要 torch)。有模型用 Beat This,没有自动回退 librosa。
| 歌 | librosa | Beat This | 真值 |
|---|---|---|---|
| 月之暗面 | 139.7 | 69.8 | 69 |
| November Rain | 156.6 | 78.9 | ~80 |
| 蓝色骏马 | 152.0 | 76.9 | — |
12 首对比:final0(82 MB)与 small0(导出 9.9 MB)有 10 首一致;small0 在 papillon 上翻倍、 final0 在 23 钢琴上翻倍——各有弱点,故选 final0(摇滚/电子更常见,且 BPM 是核心指标)。
实现:detect_tempo() 先试 Beat This(30 秒分块——Transformer 的 attention 对长序列
会吃大量内存),失败或模型缺失回退 librosa;[依据] 标「(Beat This)」,JSON 加 bpm_source。
small0 的导出脚本在 _archive/export_beat_this_small0.py(torch→ONNX,可复用)。
4. 和弦:不是"识别率低",是"性质根本分辨不出来"
先纠正一个旧数字。 以前写的"未定率 71–82%"是错的 —— 那个字段算的是
1 - 变化点数 / 块数,即**"和弦没变的比例"**,不是识别失败率。标签撒谎。
真正的测量(一首钢琴曲,126 个 2 秒块,84 个和弦模板):
| 层面 | "最佳 − 次佳"差距的中位数 | 判读 |
|---|---|---|
| 具体和弦(D#maj7 vs D# …) | 0.026 | 和噪声分不开 |
| 折叠成「根音 + 大三/小三族」 | 0.036 | 仍然很紧 |
| 只比根音(12 个) | 0.051 | 79% 的块有明显胜出者 |
结论:混音上的 chroma 只够猜根音,不够定和弦性质。七度、挂四、转位 在混音里被所有乐器互相掩蔽,再好的匹配器也只能在噪声里挑一个。
所以降级为候选模式(已实现):
- 报根音骨架:
0:00 D#(M) | 0:21.8 C(m) | …,M/m 表示大三/小三 - 性质分不清就写
(?);根音也定不下来就加?;被过滤掉的抖动段标⟨12s未定⟩ - 另报候选词表(各根音占多大比例),并明说"这是候选,不是和弦进行"
unresolved_ratio现在是真正的未定比例(根音差距 < 0.025 的块占比)[FIT] chords也改为按根音差距算 —— 它评估的是"根音骨架能信多少",不再是"和弦名对不对"
四首实测的根音未定率:钢琴曲 26% / 蒙古族乐队曲 38% / 自作曲 45%。
非西方与重混响素材明显更差,这正是 [FIT] 该反映的。
歌词 ASR 验证(2026-10-21)
问题:没有 LRC 的歌,能不能靠 Whisper 补歌词?
方法:_archive/probe_asr.py —— 4s 分轨取 vocals 轨 → faster-whisper(int8, CPU)转录 →
与 LRC 按字算 CER + 逐句对照。样本《蓝色骏马》(普通话)、《万物生(梵)》(梵语,作反例)。
结果(《蓝色骏马》,参考 141 字):
| 模型 | 体积 | 段数 | 输出字数 | 按字 CER | 表现 |
|---|---|---|---|---|---|
| tiny | ~75 MB | 17 | 120 | 79.4% | 只对副歌「好久不见」 |
| base | ~145 MB | 67 | 324 | 200% | 幻觉多,编出英文段 |
| small | ~461 MB | 15 | 820 | 548% | 前段最好,英语段循环失控 |
| small_rep | — | 29 | 313 | 199% | 抑制重复后长度减半,循环仍在 |
CER 被「输出字数爆炸」污染,看逐句更公平。small 在主歌前段确实能读出来: 「霞上山坡→向上山坡」「远远带来一只蓝色骏马→远远带来一支蓝色军马」 「好久不见 难得一见→好久不见 难得遇见」。但 131 秒后三档全崩。
梵语样本更说明问题:三档全部幻觉,且随模型变大「编得越来越像歌词」——
tiny 出「咛咢咢咢咢」,base 出英文 ounce my shadow,small 出「Zither Harp」「字幕 by 索兰娅」,
small_rep 直接编成「我愛你,你愛我」的情歌。模型不会说「我听不懂」。
结论:
- 插件内最多用 base(145 MB);small(461 MB)比 4s+6s 分轨模型总和(288 MB)还大 1.6 倍, 效果仍不可靠,不划算
- ASR 只能作为「候选 + 必须标注机器听写」,不能写进判断
- 参数调优(
no_repeat_ngram_size/repetition_penalty/temperature=0)只治输出长度, 治不了「编不编」——这不是参数问题,是模型能力边界 - 因此歌词接入顺序不变:先 LRC(35% 覆盖),ASR 只作兜底
产物:_archive/probe_asr.py、_archive/asr_probe_report.md、_archive/asr_out/(两首 vocals wav)。
实现状态(2026-10-21 下午):analyze.py --asr [tiny|base|small] 已加,默认不跑。
输出 [听写] 段:只列「较确定」的候选段;疑幻觉段连内容都不给(JSON 里 text=null)。
梵语样本实测输出「本曲没有可用的歌词段」,是正确拒绝。
注意:候选段里仍会有错字——实测《蓝色骏马》11 段候选约一半是错的(「漫個意見」实为「难得一见」)。
所以措辞是「候选」不是「可信」;最终该不该信由调用它的模型判断——这是设计,不是缺陷。
音译规则(2026-10-21):非中文人声(梵语、藏语、自语、吟唱)不得翻成中文词义—— Whisper 强制中文会产出「我愛你」式幻觉;不强制则换成语种幻觉(《蓝色骏马》整首被听成 英语「How do you feel」)。两条路都只给词、不给音。所以:
- 有 LRC(含梵文/藏文):转写罗马音或音译,标来源
- 无 LRC 且听不出词:只报时间,不给文本;转述时用音译近似(
shai-lu-way、om banza…), 并说明「这是音,不是义」 - 永远不给中文翻译,除非确认是中文唱的词
「没有可用歌词段」≠ 纯器乐:先看 [分轨] 的人声占比;有人声却读不出,就说
「有人声但词读不出,可能是外语或吟唱」,不硬编。
真人 vs AI 人声(2026-10-21 实测):
| 样本 | 候选段 | 表现 |
|---|---|---|
| 《起初说》(真人,有完整 LRC) | 18 段里 2 段 | 音对字错:「我诞于尘土」→「我的夜长途」、「奔向尘土」→「半想传途」;结构能看出是「…归于尘土」,字全错 |
| 《蓝色骏马》(真人) | 37 段里 11 段 | 副歌能对(「好久不见」),主体错 |
| AI 生成人声 | 7 段全废 | 连音都拼不成句子(「梦里欧霞」「伊莉茨」「导致山蕉的青鳳」) |
结论:真人歌上 ASR 的价值是「听出音、让你去认字」,不是直接给歌词; AI 生成人声基本读不出——合成音色、不自然的辅音与尾音不在 Whisper 训练分布里, 再加上编曲覆盖,连候选段都很少。遇到「有人声但读不出」,先问是不是 AI 生成的人声。
踩过的坑(按教训价值排序)
| # | 坑 | 教训 |
|---|---|---|
| 1 | [FIT] 说 tonality 0.05,同一份输出 [KEY] 说 0.91 |
自相矛盾的输出是最强的 bug 探测器 |
| 2 | "块平均 RMS"冒充"峰值" | 标签撒谎比数字错更危险 |
| 3 | tonality 用熵,12 个 bin 永远算出低值 | 指标要有区分力,不能算出来都一样 |
| 4 | 把"属调混淆"说成"无调性" | 体系内的语言不能用来描述体系外的问题 |
| 5 | "疑似削波"用峰值判断 | 削波要看贴顶采样点数量,不是峰值 |
| 6 | key_ambiguity 只看关系不看差距 |
0.89 的 D minor 被说成"调性不明确" |
| 7 | pip 网络请求被拦截(疑似杀软 SSL 扫描) | 用 curl 下 wheel + pip --no-index |
| 8 | 脚本选到预发布版(protobuf rc) |
pip 默认不装 pre-release,要过滤 |
| 9 | 只认 cp312-abi3 没认 cp310-abi3 |
abi3 要泛化匹配 |
| 10 | 中文路径经 bash 变量传参编码坏掉 | 硬编码路径 |
| 11 | chord_unresolved_ratio 用"和弦没变的比例"冒充未定率 |
指标名字和算式必须对得上,否则整份结论都建在错数字上 |
| 12 | fmt_time 把 239.999 秒打成 3:60.0 |
先四舍五入到目标精度再拆分,别对原始浮点取模 |
| 13 | 同一份音频被解码两遍、CQT 算两遍(4 分钟的歌 66 秒) | 先量耗时再谈优化;66s → 11s 只改了两处重复计算 |
| 14 | [FIT] tonality 0.41 不可用 旁边写"中心基本明确" |
前缀不要替调用方下结论;不可用 后面必须跟真正的理由 |
| 15 | 54 秒片段测速 65.4,全曲却是 98.7 | 短片段会锁到别的拍层级;片段 BPM 不等于全曲 BPM,输出里必须警告 |
其中 8 个是被真实样本逼出来的,不是设计时想到的。(11–15 是本轮新暴露的)
关键决策记录
MOSS-Music 8B 已决定不做。 理由:
- 模型 ~18 GB + ROCm + Python 3.12,无法作为可分享插件的依赖
- 定位应降为"可选增强",而不是核心
- 用户判断:插件不能要求别人下十几 GB
当前路线:轻量(librosa + YAMNet),总依赖 < 1 GB,CPU 可跑。
外部建议:GPT 的三轮意见(2026-10-18/19)
用户当信使,把项目现状转给 GPT 咨询。这里只记结论和判断,不抄原文。
第一轮(架构)
- 双分支:「音色复现」和「音乐理解」要的证据不同,别混一条流水线
- 测量 / 推断 / 建议 必须分开:「4.2kHz 有能量」是测量;「可能用了失真」是推断;「Mix 调 35%」是建议
- 不要先分离再决定分析什么 —— 分离伪影会污染音色判断
- 削波会伪装成失真(现场录音尤其)
- 误差传播链:分离伪影 → 错音符 → 错和弦 → LLM 用流畅语言解释错误事实
采纳:测量/推断分离(体现在 [FIT] 和"说人话"的分工)、误差传播的警惕
未采纳:双分支(还没做音色分支)
第二轮(和弦 / BPM / 动态 / 自评)
- 和弦:建议试 BTC(预训练,12.2MB 权重);Chordino 作传统基线。未做(需要 PyTorch,且要导出 ONNX)
- 三态输出:可靠识别 / 候选歧义 / 证据不足;
N/无和弦和unknown必须分开 - BPM:建议 DeepRhythm —— 未采纳,许可证 AGPL-3.0(传染性),且要 PyTorch
- 编曲动态 vs 响度动态:必须拆成两组指标 —— 已采纳并实现(
[ACT]段) - 四字段自评:
applicability/confidence/ambiguity/stability—— 部分采纳(目前只有单一[FIT]分数) - 类别缺失 ≠ 低置信度 —— 已作为原则采纳(YAMNet 没有竹笛类,不等于没有竹笛)
第三轮(风格分类)
- Essentia Discogs-EffNet:400 种风格,权重约 20MB,CPU 可跑 —— 但
- 官方 Windows Python 绑定不支持
- essentia.upf.edu 在本机完全不可达(DNS 失败)
- 分类头是
.pb(TensorFlow),要转 ONNX - 预处理必须严格复现,而参考实现装不上 → 无法验证
- 许可证 CC BY-NC-SA 4.0(非商业 + 相同方式共享,边界模糊)
- → 判定:不做
- MAEST:345MB,模型和分类头都有 ONNX(不用转)—— 太大,不做
- 轻量 HuBERT:24MB,Apache 2.0(许可干净),但只有 GTZAN 10 类 —— 可用但覆盖窄
- CLAP 体积更正:我之前说 700MB,实际音乐专用检查点约 2.35GB
- CLAP 零样本能力:GTZAN 上 Top-1 约 71%(项目自测),但没有"蒙古迪斯科"这类细分的基准
GPT 的最终判断(我认同)
只靠人工设计的 BPM / 调性 / 起音密度 / 动态 / 频谱统计,不能可靠还原文化语义。 允许用预训练模型,可以逼近其中相当一部分,但取决于模型学到了什么。
讨论中确立的六条原则
- 先给结论,数据按需 —— 默认输出是判断,不是仪表盘;数字放
[依据],明细要--detail - 敢下判断 —— 用肯定句给最可能的答案,再标置信(高/中/低), 只指出"哪一层不确定";不要每句都留退路,也不要用数据代替判断。 推断层鼓励大胆猜,但要给"反证条件"(如果是 X,你会听到 Y),让用户听一遍就能否掉
- 对用户只谈音乐,不谈管线 —— 不提
[判断]/[FIT]、不提 YAMNet、不报置信小数; 说"人声像在垫底",不说Choir 0.021 - 测量与推断分离 —— 文本输出里,哪些是量的、哪些是猜的,必须能分辨
- 不确定性显式化 —— 低分项要标(
?)、空白要标(⟨51s未定⟩)、不适用的要判"不可用" - 类别缺失 ≠ 没有 —— 模型没有"竹笛"这个类,不等于这段没有竹笛
用户明确否决的方向
| 方向 | 否决理由 |
|---|---|
| MOSS-Music 8B 本地跑 | 18GB,无法作为分享插件的依赖 |
| Gemini / Qwen API | 要钱、要梯子、麻烦 |
| 训练专用分类器(GTZAN / 民乐) | 数据老、类别不全、工作量大 |
| FL Studio 参数推荐 | 用户不需要 |
| 频段节律拆解 / YAMNet 521 类全量并进插件 | 使用频率低;插件保持轻量;需要时走临时脚本,方法见「YAMNet 的边界」一节(2026-10-21 拍板) |
校准样本(有标准答案的)
| 样本 | 人耳确认 | 工具给的 | 结果 |
|---|---|---|---|
| 一首古筝领奏的器乐曲 | 古筝、无人声、民族/东方融合器乐、偏配乐/国风电子 | 筝类拨弦;五声化根音 D–E–F#;慢热加码 + 4:32 塌 + 4:48 硬收尾;方向偏民族/配乐 | 筝 ✅ 调式 ✅ 结构 ✅ 方向 ✅ 人声 ❌(误报 Choir/Singing) 配器 ❌(CLAP 报了唢呐,实际没有) |
| 一首民乐曲 | 民乐,没有唢呐 | 声源标签里出现唢呐 | 配器 ❌ |
| 一首蒙古族乐队曲 | 确实有马头琴 | CLAP 给的是 人声 / 弦乐 / 钢琴,马头琴未进前四 | 配器 ❌(真的有的东西没认出来) |
| HAYA《papillon (Live)》 | 马头琴、冬不拉、钢琴/pad、电吉他、鼓,黛青塔娜女声嘶吼 | MusicNN: rock 0.24 / guitar 0.20 / female 0.16 |
风格 ✅(摇滚底+吉他+女声全中)、配器 ❌(马头琴/冬不拉/钢琴一个都没有 —— 标签集里没有) |
| 《The Rebel Path (cello cover.)》 | 开头大提琴,主体是 techno 编曲 | MusicNN: techno 0.40 |
✅ 对(曾误判为"模型发疯",实为人耳先入为主) |
| 《I Saw》 | 钢琴独奏 | MusicNN: piano 0.94 / slow 0.37 / solo 0.32 |
✅ 准得吓人 |
| Guns N' Roses《November Rain》 | 摇滚乐队,吉他、男声 | MusicNN: rock 0.23 / guitar 0.22 |
✅ 对 |
MusicNN 四首校准结论(2026-10-20):风格/大类判断 4/4 全对;
具体乐器只有标签集覆盖的能对(钢琴、电吉他),中国民乐一律无解。
所以它接在 [标签] 行,措辞是「模型标签(非识别)」,不进 [判断]。
三条教训:
- 具体乐器线索即使只有 0.012 也可能对(Zither→古筝);而
Choir/Singing这类容易被持续音、混响垫出来,不能当"有人声"。 - 工具写"线索偏 X,只能当方向"时,别在转述里升级成"像在垫底" —— 升级是转述者干的,不是工具的错。
- 别拿未验证的印象当"已知答案":蒙古族乐队那条写进测试表时没确认过,后来才发现预期是对的、模型是错的。
下一步候选
- 插件骨架 ✅ 已做(三个工具)
- 压缩 JSON
--brief✅ 已做(约 530 B,约 130 token) - 局部分析
audio_section(path, start, end)✅ 已做 - 和弦降级为候选 ✅ 已做(根音骨架)
- 音色分支(GPT 第一轮建议)—— 现在所有结论来自混音;要看"失真/削波/高频亮度"得单开一条
[FIT]四字段化 ——applicability/confidence/ambiguity/stability拆开(现在挤在一个分数里)- 缓存 —— 同一文件连问两段会重复解码;按
文件+修改时间缓存 chroma/能量曲线 - UI 卡片 —— 现在只有文本;DSH client 半侧能画能量条/骨架,尚未做
- 按需 stem 分离 ✅ 已做(0.6.0)——
--stems 4s/6s,基于 demucs-onnx(纯 onnxruntime, 不依赖 PyTorch)。实测 4-stem:纯器乐 vocals 约 1e-6、有人声约 25–38%,判得很干净。 模型可选安装(4s 约 150 MB fp16,6s 再加 150 MB),结果按文件缓存、重复问秒回。 仍是同步等待(短歌 60–90 秒);长歌超时提示分段。 - 歌词接入的可行性验证 ✅ 2026-10-21 已验(见「歌词 ASR 验证」一节)—— 结论:LRC 优先(23/66 = 35% 覆盖),ASR 只作兜底候选且必须标「机器听写」; tiny/base 不可靠,small 体积不划算,暂不接入插件。 LRC 是人工做的,版本可能对不上,对不上时要能发现并报出来,不能硬解释
- 从 MOSS 借东西 —— 蒸馏不可行(18 GB + CUDA,教师跑不起来)。可借:它的标签体系 (它认为音乐该有哪些属性)、它的评测方法/基准、预处理细节。借来的是“该测什么”的知识, 不是模型能力本身。先读论文/文档,不跑模型
已经定过的原则(新会话别重开)
2026-10-20 讨论的沉淀,每一条都是踩坑换来的:
- 大模型是 Teacher,不是产品 —— MOSS / SongFormer 只在开发侧跑(打标、验证), 用户永远不碰;进插件的必须是蒸馏/量化后的小模型(<100 MB)。 一度想把 MOSS 跑进插件,被用户纠正:那已经不是插件了,是另一个项目
- 搜索与先验补不了“听” —— 联网查歌手、用模型知识猜风格,都不能替代音频测量。 它只能补“这是谁”,补不了“这首听起来像什么”。 反例:知道是“cello cover”就断定它该报大提琴,结果中段是鼓 + 电音
- 先盲测后查证 —— 先只用音频出结论,再去查资料;两栏输出,永不合并
- 术语用英文 ——
drop/breakdown/fade out/hard cut,不硬翻成“抽空/砸回” - 判断里允许单个数字锚点 —— 防误读;但一行两个以上就变成甩数据
- 取件与读件分离 —— 下载音频(可能联网)和本地分析是两件事。 插件只负责“读”:“纯本地”约束的是分析,不是音频来源
- 测量与资料永不合并 —— 输出里测的、模型标的、查到的、推的,必须能分清
音乐理解的 9 个维度(借用 MOSS-Music 的 caption 评测框架)
拿它当尺子量自己,空缺一目了然。这 9 维来自 MOSS-Music 评测 caption 时用的维度集合:
| 维度 | 我们的状态 |
|---|---|
| 速度 / 节奏感 | ✅ 硬(BPM + 双层检测) |
| 结构与段落变化 | ✅ 硬(但是能量层面,不是音乐学上的 verse/chorus) |
| 配器 / 音色 | ⚠️ MusicNN,只认标签集内的;中文民乐全无 |
| 人声相关 | ⚠️ 弱,分不清唱还是念 |
| 旋律 / 和声 | ⚠️ 只有根音候选 |
| 制作 / 声学质感 | ⚠️ 响度、削波、贴顶平台,仅此 |
| 风格 / 流派 | ⚠️ MusicNN 50 类,风格大类准 |
| 情绪 / 氛围 | ❌ 空 |
| 场景 / 用途 / 语义联想 | ❌ 空 |
9 个里 2 个硬、5 个半、2 个空。 这不是问题清单,是地图 —— 下一步该补哪格,一目了然。
已知限制(用之前必须知道)
- 片段 BPM 不可当全曲速度:54 秒片段在一首钢琴曲上报 65.4,全曲是 98.7。输出会警告,但读的人仍可能忽略
- 片段统计更抖:同样的 2 秒块,整首有 126 块、片段只有 28 块,
[FIT]分数会波动 - 声源标签只到"像什么":YAMNet 给的是 AudioSet 类别,不是乐器识别;"Bowed string instrument"不等于"马头琴"。 它能给方向(像拨弦类/偏东方),但给不了名字;中文乐器误报率高(实测把唢呐报进没有唢呐的曲子), 所以判断里不写配器行,只在对话里按"推断"说
- 没有旋律、歌词、语言:音符级的内容是本地信号分析的天花板,不是 bug
- 乐器/风格没有可靠的分类方案:YAMNet(521 类事件)和 CLAP(零样本)都试过,中文乐器都是零; 参考库(音频对音频)是唯一没试过的路,但需要干净的乐器样本,且混音干扰仍在。 在找到可靠方案前,这两项只给"方向 + 置信",不给结论
No comments yet. Be the first to write one.