2026-09-22 【音色评估】项目拆解(二):环境搭建与跑通

系列定位:5 篇走完「调研摸底 → 环境跑通 → 代码拆解 → 调试改参 → 复盘沉淀」。 本篇目标:把项目从”一堆压缩包”变成「一条命令能跑、一个接口能验、一份环境手册能复刻」的可用状态。 与前面的衔接:第一篇定了”项目是什么”(双路线融合、四模块分工);第 1.5 篇补了底层知识(STFT / 梅尔刻度 / Fisher 判别 / fbank / ViT)。本篇不再重复概念,只讲实操与排错。 说明:本篇所有命令、报错、数字都来自真实复现过程(Windows + Python 3.10,项目自带
html.zip解压版)。
0. 先把”跑通”定义清楚
新手最大的误区是:窗口没报错 = 跑通了。这不成立。本文用的判据是三条,缺一不可:
| 判据 | 怎么验 | 本项目实测结果 |
|---|---|---|
| ① 服务起来了 | curl 首页状态码 | GET / → 200 |
| ② 核心接口能出结果 | 用一段真实音频 POST 核心接口 | POST /api/evaluate → 200,返回 scores / weights_scores / *_features |
| ③ 结果数值合理 | 看返回里的分数与等级是否在预期范围 | 11 类概率、三维等级 1–3、物理分数量级正确 |
附带一条经验:“能跑”和”能信”是两件事。第 ② 条只证明链路通了;第 ③ 条才说明你没把张量形状、采样率、标签顺序之类搞错。这就是为什么后面要用”对拍”再验一遍。
1. 摸清”到底要装什么”(不要照着 requirements 盲装)
项目 requirements.txt 写的是声明,而实际运行取决于 import 链。做法:把代码里所有 import 抓出来。
# 在解压后的 html/ 目录里grep -rhoE "^\s*(import|from) [A-Za-z_][A-Za-z0-9_.]*" --include=*.py . \ | sed 's/^\s*//' | sort -u这一步在本项目里抓到两个”requirements 里看不出”的关键点:
| 发现 | 为什么要紧 |
|---|---|
html/model/model_utils/stat.py 里 from sklearn import metrics | scikit-learn 是运行必需,不在直觉里的”音频/深度学习”清单上 |
timm 的顶层 import 会连带 torchvision(layers/create_norm.py) | 只装 timm 不装 torchvision,import timm 直接失败 |
顺手做的第二件事:确认哪些依赖是”训练用”、哪些是”跑推理用”。本项目里 TimbreMask/(训练框架)与 frenquence_check/(科研脚本)都不参与 Web 运行,装环境时不要被它们的 requirements 带跑偏。
2. 建一个干净环境(不要动系统 Python)
本机的现实:系统 Python 3.10 存在,但 pip 模块是坏的(python -m pip 报 No module named 'pip',pip.exe 也起不来),且没有 conda / uv。所以第一个坑就是”连包管理器都没有”。
# 用系统 Python 造一个自带 pip 的虚拟环境(venv 会自带 ensurepip)python -m venv .venv_html.\.venv_html\Scripts\python.exe -m pip --version # pip 22.0.4 ✓
# Linux/macOSpython3 -m venv venv && ./venv/bin/python -m pip --version三条硬经验:
- 虚拟环境放在项目目录内(
.venv_html/)。好处:路径可预测、便于写启动脚本、删库跑路只删一个文件夹;本项目用它同时服务html/(评估网站)和physics_lab/(实验台)。 - 别用系统 Python 装依赖:一旦装坏,全机项目一起坏,而且没法回滚。
- 环境装完先跑一次
import自检,别等到启动服务才发现少包:
python -c "import torch, torchaudio, librosa, soundfile, flask, timm, sklearn, numpy, scipy; print('imports OK')"3. 装依赖:分三批,别一把梭
按”编译依赖强度 / 下载渠道”分批,出问题好定位:
# 第 1 批:深度学习核心(必须走官方 CPU 索引,见 4.2)pip install torch torchaudio torchvision --index-url https://download.pytorch.org/whl/cpu
# 第 2 批:音频与科学计算pip install librosa soundfile numpy scipy scikit-learn numba
# 第 3 批:Web 与工具pip install flask python-dotenv本项目最终验证可用的版本组合(建议直接固化):
| 包 | 版本 | 备注 |
|---|---|---|
| Python | 3.10.3 | 3.8–3.11 均可 |
| torch / torchaudio | 2.8.0+cpu | 必须同版本号配对 |
| numpy / scipy | 1.26.4 / 1.9.3 | scipy 1.9 会警告 numpy 上界,仅警告 |
| librosa | 0.10.2.post1 | 依赖 numba/llvmlite,三者有耦合 |
| soundfile | 0.14.0(win_amd64) | 必须带 DLL 的那个 wheel |
| timm | 0.9.16 | 顶层 import 依赖 torchvision |
| flask | 3.1.3 |
4. 网络与镜像:这一步决定你是 10 分钟还是 3 小时
4.1 先测速,再选源(别凭感觉)
本项目实测(同一台机器):
| 源 | 速度 | 结论 |
|---|---|---|
pypi.tuna.tsinghua.edu.cn | ≈ 1.2 MB/s | 首选 |
pypi.org | ≈ 34 KB/s | 兜底 |
download.pytorch.org | ≈ 42 KB/s | 只在装 torch CPU 版时用 |
4.2 两个大坑(都会让你卡很久)
坑 A:pip 的远程解析在本机会”空转”。 表现为 pip 进程 CPU 一直涨、日志停在 Looking in indexes...、下载目录不增长。本项目里它空转了十几分钟、只下到 14.5 MB。
坑 B:大 wheel 的连接会被中断。 例如 torch-2.x 这种上百 MB 的文件,下载到一半停住,pip 只会重试整份文件。
解决方案:手动下载 wheel(分块 + 断点续传)+ 离线解包安装。 这是本篇最值得留下的一段代码:
# save as dl_wheels.py ;用法:python dl_wheels.py <index-url> <包名...>import os, re, sys, time, zipfile, urllib.request
def pick(base, pkg): """从 simple 索引里挑一个匹配当前平台的 wheel(优先 cp310 win_amd64,其次 py3-none-any)""" html = urllib.request.urlopen(f'{base}/simple/{pkg}/', timeout=30).read().decode('utf-8', 'ignore') hrefs = [m.group(1).split('#')[0] for m in re.finditer(r'href="([^"]*\.whl[^"]*)"', html)] best, score = None, -1 for h in hrefs: fn = h.split('/')[-1] s = 3 if 'cp310-cp310-win_amd64' in fn else (2 if 'none-any' in fn else 0) if s > score: score, best = s, h return best
def download(url, dest, chunk=4 << 20, tries=8): """按 4MB 分块下载:每块单独一个请求,断了只重试这一块""" def total(): req = urllib.request.Request(url, headers={'Range': 'bytes=0-0'}) with urllib.request.urlopen(req, timeout=40) as r: return int(r.headers['Content-Range'].split('/')[1]) n = total(); have = os.path.getsize(dest) if os.path.exists(dest) else 0 with open(dest, 'ab') as f: while have < n: end = min(have + chunk - 1, n - 1) for _ in range(tries): try: req = urllib.request.Request(url, headers={'Range': f'bytes={have}-{end}'}) with urllib.request.urlopen(req, timeout=120) as r: data = r.read() f.write(data); f.flush(); have += len(data); break except Exception: time.sleep(1.5) else: raise IOError('stuck at %d' % have) return n
if __name__ == '__main__': base, pkgs = sys.argv[1], sys.argv[2:] os.makedirs('wheels', exist_ok=True) for pkg in pkgs: h = pick(base, pkg); fn = h.split('/')[-1] url = base + '/' + h.lstrip('./').replace('../', '') dest = os.path.join('wheels', fn) print(pkg, '->', fn, download(url, dest), 'bytes')装的时候直接解包(等价于 pip 安装,但跳过元数据解析,因此不会被”Requires-Python 不满足”之类的元数据问题一把拒绝整批):
import glob, os, zipfileSP = r'.venv_html\Lib\site-packages' # Linux: .venv/lib/python3.x/site-packagesfor wh in sorted(glob.glob('wheels/*.whl')): if os.path.basename(wh).startswith(('torch-', 'torchaudio-')): # 已装的可跳过 continue zipfile.ZipFile(wh).extractall(SP) print('installed', os.path.basename(wh))⚠️ 用这种方式装,依赖不会自动补齐(这就是第 5 节里几个
ModuleNotFoundError的来源)。代价换来的好处是:100% 可控、可断点续传、可离线复现。
5. 版本地雷清单(全部是本项目真实踩过的)
按”现象 → 根因 → 解决”给表,遇到直接查:
| # | 现象 | 根因 | 解决 |
|---|---|---|---|
| 1 | No module named 'pip' | 系统 Python 的 pip 被破坏 | 用 venv(自带 pip) |
| 2 | pip 卡在 Looking in indexes 空转 | 本机 pip 远程解析异常 | 手动下载 wheel + 离线解包(4.2) |
| 3 | Linux 上 pip install torch 拉 2.4 GB | PyPI 的 Linux torch 是 CUDA 构建 | 加 --index-url .../whl/cpu |
| 4 | ImportError: TorchCodec is required for load_with_torchcodec | torchaudio ≥2.9 默认改用 TorchCodec | 降到 torchaudio 2.8(sox 后端) |
| 5 | Could not load this library: libtorchcodec_core7.dll | 装了 torchcodec 但原生 DLL 起不来 | 直接移除 torchcodec,用 2.8 |
| 6 | RuntimeError: operator torchvision::nms does not exist | torchvision 新版 _C_stable 与 torch CPU 构建不兼容 | 见下方”垫片”方案 |
| 7 | ModuleNotFoundError: torchvision(来自 timm) | timm 顶层 import 需要 torchvision | 装配套 torchvision,或写最小垫片 |
| 8 | No module named 'typing_extensions' | --no-deps 安装 torch 没带依赖 | 补 typing-extensions 等 |
| 9 | ImportError: cannot import name 'TypeIs' from 'typing_extensions' | typing_extensions 版本过旧(rc 版) | 升级到 4.16.0 |
| 10 | ValueError: invalid literal for int() with base 10: '6rc1' | numba 解析 colorama 0.4.6rc1 版本号崩溃 | 装稳定版 colorama 0.4.6 |
| 11 | No module named 'decorator' / 'cloudpickle' | librosa / joblib 的隐式依赖 | 补装 |
| 12 | sndfile library not found / cannot load library 'libsndfile.dll' | 装了不带 DLL 的 soundfile wheel | 用 soundfile-0.14.0-py2.py3-none-**win_amd64**.whl(内含 _soundfile_data/libsndfile_x64.dll);Linux 装 libsndfile1 |
| 13 | A NumPy version >=1.18.5 and <1.26.0 is required ...(仅警告) | scipy 1.9 元数据写的上界 | 可忽略;要干净就 numpy 1.25.x 或升级 scipy |
| 14 | torch.load 报 “Weights only load failed” | PyTorch ≥2.6 默认 weights_only=True | 显式 torch.load(..., weights_only=False) |
| 15 | 装依赖奇慢(10 分钟才拷完一个包) | 项目在网盘同步目录里,小文件写入极慢 | 加 --no-compile、少装可选项;必要时换本地盘 |
第 6 项的”垫片”方案:当 torchvision 的原生扩展与 torch CPU 构建对不上、而项目运行时并不真的调用 torchvision(只是 timm 顶层 import 需要它)时,可以放一个最小占位模块,把 import 链喂饱即可:
__version__ = '0.0.0-shim'# ops/misc.py 里提供 timm 需要的符号class FrozenBatchNorm2d: # timm.layers.create_norm 只在本项目不走的路径上引用它 def __init__(self, num_features, eps=1e-5): ...# datasets/__init__.py 里给 timm.data.dataset_factory 需要的桩类CIFAR10 = CIFAR100 = MNIST = KMNIST = FashionMNIST = ImageFolder = object# transforms/functional.py 里提供 InterpolationMode 枚举这不是”最佳实践”,而是”在受限环境下让 import 链通起来的止血方案”;能装配套 torchvision 时优先装配套。关键是:要清楚自己在绕什么,并在文档里写清楚。
6. 数据与资源准备(跑通前必须核对的两件事)
6.1 模型权重:先校验字节数,再谈运行
stat -c%s html/model/checkpoint-300.pth # Linux(Get-Item html\model\checkpoint-300.pth).Length # Windows# 期望:1024918304本项目这个文件约 1.02 GB。顺便一个很有用的认知(复现时能省很多疑惑):
ViT-B 约 86M 参数,fp32 权重只有 ≈345 MB,而文件是 3 倍体量 → 里面约 2/3 是 AdamW 优化器状态。所以:① 加载要 10–20 秒;② 发布时可以只保存
model权重把体积压到 1/3。 校验字节数这一步,能直接排除”下载/解压被截断”这一类最难查的问题。
6.2 测试音频:自己造一条”确定性”的
用真实录音做回归测试会引入变量(内容/环境/设备)。本项目用一段合成音频做冒烟测试(12 秒、分段基频 + 谐波 + 少量噪声、16 kHz/16 bit):
import numpy as np, soundfile as sfsr, dur = 16000, 12t = np.arange(sr * dur) / srseg = (t // 2 % 3) + 0.5 # 每 2 秒换一个基频f0 = 110 * seg * (1 + 0.03 * np.sin(2 * np.pi * 5 * t))y = sum((1.0 / h) * np.sin(2 * np.pi * f0 * h * t + 0.5 * h) for h in range(1, 7))y = 0.35 * y + 0.06 * np.random.default_rng(42).normal(size=len(t))sf.write('test_input.wav', np.clip(y, -1, 1).astype('float32'), sr, subtype='PCM_16')它的价值:波形已知、时长已知、每段的基频已知。一旦推理输出与上次不同,就能确定是环境/代码变了,而不是音频变了。
7. 首次运行:“跑通三判据”逐条验
cd html..\.venv_html\Scripts\python.exe app.py # 或 python app.py# 控制台出现:Running on http://127.0.0.1:5001# 判据 ① 首页curl -s -o /dev/null -w "首页 HTTP %{http_code}\n" http://127.0.0.1:5001/# → 首页 HTTP 200
# 判据 ②③ 核心接口(multipart 传音频)curl -s -X POST http://127.0.0.1:5001/api/evaluate -F "audio=@../test_input.wav" | head -c 400本项目实测返回(片段):
{"scores": {"thickness": 3, "solidity": 3, "brightness": 3}, "scores_change": {"thickness": 0, "solidity": 0, "brightness": 0}, "weights_scores": {"thickness": 80.29, "solidity": 541.92, "brightness": 9400.03}, "thick_features": [ ... 12 维 ... ]}首请求耗时:模型加载 + 第一次推理约 9.7 秒(CPU),之后的请求快得多。把这个数字记下来 —— 它是你判断”环境是否正常”的基准线。
顺手记两条”跑通即暴露”的问题,它们在这一步才会出现(第三篇会详细拆):
- 浏览器录音上传必 500:
MediaRecorder录出的是 WebM/Opus,而librosa/libsndfile 不认这个容器 → 需要在前端转成 WAV再上传;- 物理链路在某个分支崩:模型判 C 级且物理分偏高时会进”C→B 跨级”分支,两组权重与特征长度不匹配 →
ValueError: operands could not be broadcast together with shapes (12,) (13,)。这正是”跑通”的边界:能跑 ≠ 所有分支都跑得通。第三篇拆代码、第四篇做调试,都是在补这个差。
8. 环境固化:让”下次还能量出来”
8.1 锁版本(比 requirements 更硬)
pip freeze > requirements.lock.txt把第 5 节那张版本表写进项目 README,标注”实测可用组合”。深度学习生态半年一变,不锁版本 = 半年后重装跑不起来。
8.2 一键启动脚本(3 个真实的 Windows 编码坑)
本项目做启动器时踩了三个坑,跨平台都值得注意:
| 现象 | 根因 | 解决 |
|---|---|---|
'ython.exe' 不是内部或外部命令 | .bat 存成 UTF-8,而 cmd 按 GBK(936) 解析 → 中文字节”吃掉”了相邻的 ASCII 字符(python.exe → ython.exe),变量 %PY% 随之损坏,最后回退到没有依赖的系统 Python,报 No module named 'flask' | 批处理内容全用纯 ASCII(提示信息写英文),中文只出现在文件名 |
call "...\?????.bat" 找不到目标 | 文件内容里写了中文文件名,而文件按 ASCII 保存 → 中文变成 ? | 真实脚本用 ASCII 名(start_web.bat),中文名文件只作入口 |
| 控制台中文乱码 | Python 输出 UTF-8,cmd 按 GBK 显示 | 脚本头部加 chcp 65001 >nul,并设 PYTHONUTF8=1 / PYTHONIOENCODING=utf-8 |
可复用的启动器骨架(纯 ASCII):
@echo offchcp 65001 >nulcd /d "%~dp0"set "PY=%~dp0..\.venv_html\Scripts\python.exe"set PYTHONUTF8=1if not exist "%PY%" ( echo [ERROR] venv python not found: "%PY%" pause exit /b 1)echo Open in browser: http://127.0.0.1:5001"%PY%" app.pypause >nul8.3 自检脚本(把”跑通”变成可重复动作)
本项目留了两个,思路可以直接搬:
| 脚本 | 作用 |
|---|---|
physics_lab/selftest.py | 不开浏览器跑完全部实验,打印关键数字 |
physics_lab/diag_bad_inputs.py | 构造 13 种异常输入(空文件 / 二进制 / WebM / 全静音 / 超短 / 削波…),检查接口是否都给出可读的 400 而不是 500 |
这类”异常输入清单”值得每个项目都建一份 —— 它把”我试过了报错”变成”我知道它为什么报错、报什么、怎么提示用户”。
9. 本篇产出物(可直接复用的东西)
- ✅ 一个 100% 可运行版本:
html/+.venv_html/(已验证版本组合) - ✅ 环境搭建手册:第 3、5 节(分三批安装 + 15 条地雷清单)
- ✅ 离线安装资产:
wheels/目录(全部依赖 wheel,断网也能装) - ✅ 可复用下载器:
dl_wheels.py(分块 + 断点续传) - ✅ 测试资产:
test_input.wav(确定性合成音频) - ✅ 启动器:
启动音色评估网站.bat/启动物理链路实验台.bat(纯 ASCII + 绝对路径 + UTF-8 控制台) - ✅ 自检与异常清单脚本:
selftest.py/diag_bad_inputs.py
10. 报错速查(按症状索引)
| 症状 | 先看 |
|---|---|
ModuleNotFoundError: No module named 'xxx' | 是不是用 --no-deps/离线解包装的?按第 5 节补依赖 |
import 成功但启动就退 | 端口占用(netstat -ano | findstr :5001);或 venv 路径写错 |
| 首页 200 但接口 500 | 打开后端日志看 traceback 最后几行;再按第 7 节两条典型问题排查 |
| 接口 500 但错误信息为空 | 典型是 NoBackendError(无消息)→ 说明音频容器不支持(WebM/M4A),需要前端转码 |
结果里出现 NaN | 分母为 0(静音)导致;NaN 不是合法 JSON,必须显式兜底 |
| 分数是负数或几千分 | 不是崩溃,而是”线性映射外推”(第四篇会专门拆这个) |
| 加载模型极慢 | 1 GB checkpoint 的正常表现;首次 10–30 秒 |
11. 本篇收尾与下一篇预告
收尾目标达成:项目从”压缩包”变成了「有环境、有版本锁定、有一键启动、有自检脚本、有验证记录」的可运行版本;同时明确了”跑通”的边界 —— 接口通了,但某些分支还没被覆盖(浏览器录音、跨级修正)。
**下一篇(第三篇:逐行拆解代码)**会顺着本篇留下的两个”未覆盖分支”往下挖:
- 从
app.py→talk.py→inference.py→speechprocess.py的完整调用链,每段的入参/出参/形状变化; - 两条”同一项目里并存”的音频管线(推理侧 fbank vs 物理侧 STFT 频段占比)为什么要分开;
- 物理链路的六套模板与
bands_change常量,到底是从哪几个科研脚本”抄”过来的; - 给核心文件加中文注释的方法:“把别人的代码变成自己能讲出来的代码”。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
本文链接
https://study.aokana.cn/posts/2026-09-22-音色评估-项目拆解-二-环境搭建与跑通-160/评论区
一起讨论,一起进步
交流想法,也欢迎提出建议。
评论加载中…









