把 faster-whisper 模型从 C 盘搬到移动硬盘:一个 bat 脚本的完整拆解
背景:faster-whisper 的 large-v3-turbo 模型(约 1.6GB)默认缓存在 C 盘的 HuggingFace 目录里,C 盘空间紧张,想搬到移动硬盘
E:\models\下,且搬完的文件夹要能被 faster-whisper 直接加载。 本文逐段拆解move-whisper-model-to-E.bat,顺带记录 batch 脚本里三个容易踩的坑。
一句话结论
HuggingFace 缓存里 snapshots 目录下的文件全是符号链接/硬链接,直接复制大概率拿到"断链"目录;脚本的核心就一招——用 robocopy 默认的"解引用"行为复制真实文件,外面再包四道保险:占用检查 → 盘符检查 → 文件校验 → 交互确认后才删源。
一、问题:为什么不能直接复制 HF 缓存?
先看 HuggingFace 缓存的真实结构:
%USERPROFILE%\.cache\huggingface\hub\
└── models--mobiuslabsgmbh--faster-whisper-large-v3-turbo\
├── blobs\ ← 真实文件本体,文件名是 sha256 哈希
├── refs\ ← main → commit hash 的指针
└── snapshots\
└── <commit-hash>\ ← 这里全是链接,不是真文件!
├── model.bin → ..\..\blobs\<hash1>
├── config.json → ..\..\blobs\<hash2>
└── ...
| 复制方式 | 结果 |
|---|---|
| 资源管理器拖拽 snapshots 文件夹 | ⚠️ 行为不统一:硬链接一般会复制成真实文件,符号链接可能复制"链接本身",跨盘后指向不存在的相对路径 → 死链 |
| 压缩软件打包 | ⚠️ 同上,取决于工具对 symlink 的处理 |
huggingface-cli download 重新下载 |
✅ 干净,但要再耗 1.6GB 流量 |
| robocopy(默认参数) | ✅ 默认解引用符号链接,复制的是链接指向的真实内容,产物是自包含的普通文件夹 |
这就是脚本选择 robocopy 的根本原因:不是因为它快,而是因为它的默认行为恰好是我们要的"解引用"。
二、脚本总览:6 步流程
0) 占用检查 ── python.exe 在跑就中止
1) 源检查 ── C 盘缓存 snapshots 目录存在?
2) 盘符检查 ── 目标盘(默认 E:)已连接?
3) 定位 ── 找到 snapshots\<commit-hash> 版本目录
4) 复制 ── robocopy 解引用复制(核心步骤)
5) 校验 ── 关键文件清单 + model.bin 字节数下限
6) 清理 ── 交互确认后删除 C 盘原缓存(可选)
支持两种用法:
:: 双击运行 —— 用默认目标 E:\models\faster-whisper-large-v3-turbo
move-whisper-model-to-E.bat
:: 带参数 —— 自定义目标路径(含空格记得加引号)
move-whisper-model-to-E.bat "D:\ai\models\whisper-turbo"
三、逐段拆解
0) 占用检查:先确认没人正在用模型
tasklist /FI "IMAGENAME eq python.exe" 2>nul | findstr /i "python.exe" >nul
if not errorlevel 1 (
echo [中止] 检测到 python.exe 正在运行...
exit /b 1
)
tasklist /FI按镜像名过滤,findstr再确认一次(防止 tasklist 的"INFO: No tasks"提示干扰判断);if not errorlevel 1意思是"错误码 < 1",即 findstr 找到了 python 进程 → 中止。
⚠️ 这是保守策略:哪怕 python 跑的是无关任务也会拦下来。模型文件 1.6GB 复制中被打断虽不致命(有第 5 步校验兜底),但重跑很浪费时间,宁可误拦。
1-2) 源与目标盘检查
if not exist "%SRC%\snapshots" ( ... 中止 ... )
for %%D in ("%DEST%") do set "DRV=%%~dD"
if not exist "%DRV%\" ( ... [中止] 未找到 %DRV% 盘, 请确认移动硬盘已连接 ... )
两个 batch 惯用法值得记住:
%%~dD:从完整路径里只提取盘符(E:),再检测E:\是否存在——比直接if exist %DEST%更精准,能把"移动硬盘没插"和"路径还没建"区分开;%~1:取第一个参数并自动去掉两侧引号,实现"默认值 + 参数覆盖"。
3) 定位 snapshot 版本目录
set "SNAP="
for /d %%d in ("%SRC%\snapshots\*") do set "SNAP=%%d"
for /d 遍历 snapshots 下的子目录,循环里反复覆盖 SNAP,最终留下最后一个枚举到的目录。单个模型通常只有一个 commit 版本目录,这样写最省事;若真有多个版本,取的是按名称排序的最后一个(commit hash 无时间语义,见第四节坑 3)。
4) 复制:一行 robocopy 干全部(核心)
robocopy "%SNAP%" "%DEST%" /R:1 /W:1 /NDL /NP /NJH
if errorlevel 8 ( ... [失败] ... )
| 参数 | 含义 | 为什么 |
|---|---|---|
| (默认行为) | 解引用符号链接 | 本文的立脚本之意,复制 blobs 里的真实文件 |
/R:1 /W:1 |
失败重试 1 次、间隔 1 秒 | 默认是重试 100 万次 × 等 30 秒——文件被占用时脚本会"卡死"几小时,必须改小 |
/NDL /NP /NJH |
不列目录、不显示进度、不打作业头 | 1.6GB 几个大文件,关掉噪音输出,日志只剩关键行 |
if errorlevel 8 |
返回码 ≥ 8 才算失败 | robocopy 的返回码 0–7 全是成功(1=已复制文件,2=有多余文件,4=有 mismatches……按位叠加),用 if errorlevel 1 判断会误杀 |
5) 校验:不信任复制过程的"成功"
for %%F in (model.bin config.json tokenizer.json vocabulary.json preprocessor_config.json) do (
if exist "%DEST%\%%F" (echo [OK] %%F) else (echo [缺失] %%F)
)
for %%F in ("%DEST%\model.bin") do set "SZ=%%~zF"
if %SZ% LSS 1500000000 ( ... [失败] model.bin 小于 1.5GB ... )
两层校验:
- 清单校验:faster-whisper 加载必需的 5 个文件逐一确认存在,缺任何关键文件立刻报出来;
- 大小校验:
%%~zF取文件字节数,model.bin 实际约 1.6GB,低于 1.5×10⁹ 字节说明复制不完整(比如移动硬盘中途掉线、exFAT 写入出错)。
✅ "复制成功 ≠ 复制完整",对跨 USB 设备的大文件操作,事后校验是必须的。
6) 可选清理:删除前先问一句
set /p "DELC=是否删除 C 盘原缓存以释放约 1.6GB? (Y/N): "
if /i "%DELC%"=="Y" (
rd /s /q "%SRC%"
if exist "%SRC%" (echo [警告] 删除失败, 可能有文件被占用。) else (echo 已删除: %SRC%)
)
三个细节:
- 删除是交互确认的,不是无条件执行——脚本自己就遵守了"先确认再删"的原则;
rd /s /q后复查if exist:目录若被占用,rd可能静默失败,必须验证结果而不是假设成功;- 删的是整个
models--...目录(含 blobs 和 snapshots),一次释放完整空间。
最后脚本把加载示例直接打印在屏幕上,防止一个月后忘了路径和参数:
from faster_whisper import WhisperModel
model = WhisperModel(r"E:\models\faster-whisper-large-v3-turbo", device="cpu", compute_type="int8")
WhisperModel 第一个参数直接吃本地目录路径,命中后完全离线,不再访问 HuggingFace;compute_type="int8" 把内存占用从 fp16 的 ~3.2GB 压到 ~1GB 上下,CPU 推理场景的标准搭配。
四、三个 batch 坑点(划重点)
| # | 坑 | 说明 | 本脚本的对策 |
|---|---|---|---|
| 1 | robocopy 返回码 0–7 都算成功 | 1、2、4 等是"部分完成/有差异"的正常码,if errorlevel 1 会把成功当失败 |
用 if errorlevel 8,只有 ≥8(复制失败/严重错误)才中止 |
| 2 | IF 数值比较是 32 位有符号整数 |
上限 2,147,483,647 字节 ≈ 2.0GiB。本脚本 model.bin 约 1.6×10⁹ 字节,恰好安全;若照搬去迁移 >2GB 的大模型,%%~zF 的值会溢出导致比较结果错乱 |
用 1.5×10⁹ 作下限正好卡在 int32 范围内;迁移更大模型时需改用 PowerShell (Get-Item).Length 比较 |
| 3 | for /d ... do set 取的是"最后一个" |
多版本快照时拿到的是名称排序的末尾,commit hash 排序与下载时间无关 | 单模型单版本场景无影响;通用化时应枚举全部并让用户选 |
另有两个环境层面的注意点:
- ⚠️ 移动硬盘文件系统:exFAT/NTFS 均可,1.6GB 远低于 FAT32 单文件 4GB 上限,但 FAT32 单次写入大文件更容易出错,第 5 步的大小校验就是为此兜底;
- ⚠️ 盘符漂移:移动硬盘换 USB 口后盘符可能从 E 变 F,好在脚本支持参数覆盖目标路径,也可以在做完符号链接(见下)后固定用目录路径加载。
五、方案对比:为什么不换个更"时髦"的办法
| 方法 | 产物形态 | 主要缺点 |
|---|---|---|
| 资源管理器复制 snapshots | ⚠️ 可能带死链 | symlink 行为不可控,出问题不易察觉 |
huggingface-cli download --local-dir |
✅ 普通文件夹 | 要重新走一遍 1.6GB 下载 |
整个 hub 缓存目录搬走 + 设 HF_HOME 环境变量 |
✅ 可行 | 加载代码仍依赖 HF 的缓存机制,目录结构冗余(blobs+links 双份体积感知);换机器要重新配环境变量 |
| robocopy 解引用复制(本脚本) | ✅ 自包含普通目录 | 需要一段 batch;无 |
robocopy 方案的产物是"最干净"的:一个纯粹的模型文件夹,拷到任何机器、任何框架都能直接按路径加载,不依赖 HuggingFace 的缓存布局。
小结
这个脚本值得复用的不只是"搬 whisper 模型"这一件事,而是一套跨盘迁移大文件的通用套路:
- 动手前:检查占用(进程在跑就停)→ 检查源 → 检查目标盘;
- 复制时:选对工具语义(robocopy 解引用)→ 改小重试参数防卡死 → 返回码 ≥8 才算失败;
- 复制后:关键文件清单校验 + 字节数下限校验,不信任过程的"成功";
- 清理时:删除前交互确认,删除后复查结果。
凡是"HF 缓存 / pip 缓存 / npm 缓存搬家到移动硬盘"的场景,把第 11–12 行的 SRC / DEST 换掉,第 68 行的文件清单换掉,这套骨架基本可以原样复用。
posted on 2026-09-20 21:31 fox_charon 阅读(14) 评论(0) 收藏 举报
浙公网安备 33010602011771号