Host hit 0 GB physical free / 76.8 of 79.6 GB commit while both tiers ran (pagefile capped at 32 GB on a 97%-full D:), and the Dokan write of the batch's largest output raised MemoryError. except OSError does not catch that, so the item was logged '!! FAIL MemoryError:' with an empty message and the complete 493 MB <live>.new.mp4 stayed on the share; 8 such orphans had accumulated. - _sha_remote / copy_remote_to_local / copy_local_to_remote: except (OSError, MemoryError) so a transient host hiccup retries instead of failing the item - same widening for both partial-file cleanups - per-item handler: delete <dest>.new.mp4 unconditionally (os.remove + ignore FileNotFoundError, never gated on os.path.exists - stale metadata on this share) Verified by injecting MemoryError: the upload path returns False with the partial deleted, the source read retries then raises a clean IOError, the hash retries twice.
161 lines
11 KiB
Markdown
161 lines
11 KiB
Markdown
# STATE — 樊登读书会媒体库转码(2026-10-09 11:40 更新)
|
||
|
||
给下一个会话的交接。**权威运行器:`scripts/library_run.py`**(不要再恢复旧的 `hevc_lib.py`)。
|
||
|
||
## 现在在跑什么
|
||
|
||
**两档都停着** —— 2026-10-09 11:30 业主叫停,先修 bug 再重启;进程已按 PID 全部清掉,GPU encoder 0 %。
|
||
**重启前先读「第 10 个 bug:MemoryError」和「8 个 `.new.mp4` 残留」两节**,并先腾内存。
|
||
|
||
原设计:两个 HEVC 档在跑,各自独立的锁(`run-x265.lock` / `run-nvenc.lock`;x265 = E 核,nvenc = GPU ASIC,互不争抢)。
|
||
|
||
**它们不在 Hermes 的进程管理里** → `process_manage` 看不到,也没有退出通知:只能看日志、按 PID 杀。
|
||
|
||
```bash
|
||
# 断点续跑(幂等:已转好的自动跳过,失败项文件未动,下一轮自动重做),每档最多重试 8 次
|
||
bash /d/tmp/xcode/relaunch_tiers.sh
|
||
|
||
# 看进度
|
||
tail -f /d/tmp/xcode/run_x265.log /d/tmp/xcode/run_nvenc.log
|
||
```
|
||
|
||
### 为什么改成"脱管"启动 —— 10:37 那次全停的教训(第 9 个 bug)
|
||
|
||
上一轮的 x265 / nvenc 是 Hermes 后台进程。**用户停会话时它们被一起杀了**:两个都停在编码到一半
|
||
(stage 里 `*_out.mp4` 和 `.err` 停在 10:37,日志里没有 `PASS-DONE`),所以 `persist_on_release` 在这个
|
||
环境里**不可靠**。现在两档由 `powershell Start-Process` 拉起的独立 bash 启动,父进程立刻退出 →
|
||
进程树成为孤儿,**Hermes 的清理碰不到它们**。
|
||
|
||
代价与规矩:没有退出通知(看日志);**停/杀只能按 PID**,绝不用进程名批量杀。
|
||
|
||
## 进度(2026-10-08 10:50,库内共 589 个文件)
|
||
|
||
| 档 | 已转好 | 待转 |
|
||
|---|---|---|
|
||
| copy | 160 | 4(见下) |
|
||
| x265 | 13 | 169 |
|
||
| nvenc | 25 | 174 |
|
||
| 无需处理(已达播发标准) | 44 | — |
|
||
| **合计** | **242** | **347** |
|
||
|
||
- 预期:x265 ~3.5 天、nvenc ~3 天跑完各自那档(这一轮 10:43 才重新起来,10:37 被上面那次全停打断)。
|
||
- 台账:`C:\AI\transcode-run\ledger.jsonl`(每项一行:分类/结果/大小/SSIM)。
|
||
- 进度只认台账与 state,**不看 `.mp4` 扩展名**(TubeSync 重命名会把它改回 `.mkv`)。
|
||
|
||
## 2026-10-08 新增的 4 集"每周更新"(copy 档)
|
||
|
||
`1004 反内卷` `0912 感官觉醒` `0905 如何成为情绪稳定的父母` `0822 超赞的一代`(都在 `樊登读书2026年(更新中)`):
|
||
H.264 + **mp3** 音轨,其中 `0822 超赞的一代` 其实是 **MPEG-TS** 只是挂着 .mp4 名 → iPad 播不了,
|
||
需要 copy 档(视频逐位不变 + 音轨转 AAC)。
|
||
|
||
⚠ **不要跑整轮 `--tier copy`**:copy 档唯一的跳过判据是"已经是 HEVC",而 copy 的产物是 H.264 ——
|
||
整轮会把 193 个已转好的 copy 文件全部重做一遍。改用单文件通道:
|
||
|
||
```bash
|
||
bash /d/tmp/xcode/convert_new4.sh # 逐个 --tier copy --path <file> --keep-source,共用 run-copy.lock
|
||
```
|
||
|
||
- `1004 反内卷` 10:48:53 完成 ✓(535 MB → 535 MB,视频 100% 逐位不变,音频转 AAC,149 s,rc=0)
|
||
|
||
## 图片闸门:re-do 用 0.975(业主决定 2026-10-08)
|
||
|
||
第一轮实测:一趟里 11 次编码**只差在 mean**(0.9655–0.9795)就被拒,而被接受的那些帧最低分反而更差
|
||
(0.33–0.90)—— 阈值在切一条连续的分布,不是在分开缺陷(`0718 马斯克原理`:min 0.887、**0.00%** 帧
|
||
<0.85,照样被拒)。被拒的文件本来就已能播(HEVC hvc1 + AAC),re-do 的唯一目的是清掉旧设定的膨胀;
|
||
而要够到 mean 0.98 得花掉超过源片自身的码率,会被 size ceiling 拦住 —— 结果是文件连同膨胀被永久搁置,
|
||
而且每一趟还白烧一次编码。
|
||
|
||
新规则(`library_run.SSIM_MEAN_MIN_REDO = 0.975`):**只有**"重做一个已经在播的文件"才用 0.975,条件为
|
||
live = HEVC hvc1 **且**音轨是 AAC(真能播)**且**本次是从快照原档重做;首次转换仍用 0.98。
|
||
帧占比判据(>1% 帧 <0.85)两档都不变 —— 它才是抓真缺陷的那条:`失控`(0.9655)仍会被拒,
|
||
`0808 陌生人效应`(0.9739)因为 live 是 H.264、根本不是 re-do,也仍走 0.98。
|
||
先在一个文件上验证(`0321 随他们去`,上一轮 mean 0.9789 被拒)→ 见 `/d/tmp/xcode/run_pilot.log`。
|
||
|
||
## 目录与日志
|
||
|
||
| 用途 | 路径 |
|
||
|---|---|
|
||
| 运行日志 | `/d/tmp/xcode/run_x265.log`、`run_nvenc.log`、`run_copy.log`、`run_new4.log` |
|
||
| 脱管启动器 / 新集转换 | `/d/tmp/xcode/relaunch_tiers.sh`、`convert_new4.sh`(+ `relauncher.out`);**源在仓库 `run/`**(改仓库那份再拷出来) |
|
||
| 单文件日志 | `/d/tmp/xcode/run_single.log` |
|
||
| 暂存目录(编码/闸门 raw) | `C:\AI\transcode-run\stage`(C: 盘,要留 ≥8 GB) |
|
||
| 台账(每项一行 JSON) | `C:\AI\transcode-run\ledger.jsonl` |
|
||
| 锁(每档一个 + 心跳) | `C:\AI\transcode-run\run-<tier>.lock` |
|
||
|
||
## 本轮修掉的 bug(按时间顺序,全部已提交推送)
|
||
|
||
1. **闸门判据太粗糙** → 改"缺陷形状"判:`mean < 0.98` 或 `>1% 帧 <0.85` 才判错,并**剔除近平坦帧**(SSIM 在黑场/淡入淡出上无意义;实测:好编码 0/1500、误判文件 0.33%、真缺陷 2.9%)。
|
||
2. **闸门用输入 seek 解码** → 改**两侧顺序解码**(open GOP 源从中间 seek 会解出错误画面:同一文件与自身比对 min 0.25)。
|
||
3. **闸门 raw 文件固定名** → 三个档并行时互踩(实测报出"100% 帧不符"的假缺陷)→ 改 **PID + tag** 命名。
|
||
4. **安装只用 ffprobe 元数据复核** → RaiDrive 返回**缓存属性**,rename 没落地也说成功(实测"ok 1085→478 MB"而线上仍是原档)→ 改**安装后回读哈希**。
|
||
5. **源拷贝只校验字节数** → 加 **SHA-256 比对共享盘副本**(长度对内容错是真风险)。
|
||
6. **probe 单次失败即判文件失败** → 加**重试 3 次**。
|
||
7. **看门狗用 stderr 管道**(本轮最贵的 bug)→ ffmpeg `-stats` 每 0.5 s 一行,64 KB 管道几分钟塞满 → ffmpeg 阻塞在 write() → 输出不增长 → 看门狗**误杀好编码**(整夜 ~270 杀、只转成 24 个、GPU 0%)。修:**stderr 写文件**,阈值 5→15 分钟。自检脚本 `D:\dev\hoelee-tubesync-management\cache\watchdog_selfcheck.py`。
|
||
8. 看门狗**保留**(它要防的是真实场景:用户更新 NVIDIA 驱动会把在跑的 NVENC 冻死 1.5 小时且不报错)。
|
||
9. **Hermes 后台进程会随会话一起被杀** —— 两个档在 10:37 同时死在编码中,日志里没有 `PASS-DONE`;改脱管启动(见上)。
|
||
|
||
## 未完成 / 待办
|
||
|
||
- [ ] 两档跑完 → **全库落地审计**:`D:\dev\hoelee-tubesync-management\cache\install_audit.py`(核对线上文件真实大小 + 编码器;曾一次抓到"报告成功没落地")+ `copy_land_check.py`(copy 档用音轨 aac/mp3 判定)。
|
||
- [ ] 失败项:无需清单,新一轮自动重做(失败时文件原样)。
|
||
- [ ] `2022年/0723 《走出强迫症》/走出强迫症.mp4` 用新下载(`D:\Download\0723走出强迫症.mp4`,618,696,954 B)已转好装好(450,502,893 B,HEVC+AAC,SSIM min 0.997,时长 3399.81 s)✓。**快照里那份仍是坏的**(618,697,011 B,moov 缺失)→ 待定:是否用干净副本替换快照里的坏档,以免将来"从原档重转"踩坑。
|
||
- [ ] 目标文件夹里曾出现一个 0 字节的 `0723走出强迫症.mp4`(现已不在)—— 若是用户自建请告知命名约定。
|
||
- [ ] 全库唯一真损坏:`走出强迫症.mp4` 的原档(上面已处理);其余 573 个可读文件截断审计 0 可疑。
|
||
|
||
## 用法速查
|
||
|
||
```bash
|
||
# 单文件(任意位置)转好并装回库,走同一套分档/闸门/回读
|
||
python scripts/library_run.py --path "D:/Download/x.mp4" --dest "Z:/.../x.mp4" --keep-source
|
||
|
||
# 只审计不落地(本地编码+闸门,输出丢弃)
|
||
python scripts/library_run.py --tier x265 --limit 3 --keep-local
|
||
|
||
# 分档预览
|
||
python scripts/library_run.py --dry-run --limit 20
|
||
```
|
||
|
||
## 第 10 个 bug:MemoryError 会在共享盘上留孤儿(2026-10-09)
|
||
|
||
现象:11:19:31 与 11:19:37 连续两次 `!! FAIL MemoryError:`(消息空白),两次相隔 6 秒 ——
|
||
一次是《崇祯传》**装盘**(写 493 MB 到 RaiDrive),一次是下一项**读源**。
|
||
|
||
根因是**整机内存/提交额度耗尽**,不是文件问题:
|
||
|
||
```
|
||
物理可用 0.0 GB / 48 GB 已提交 76.8 GB / 上限 79.6 GB(96.5 %)
|
||
pagefile d: 初始 8 GB / 上限 32 GB —— 但 D: 只剩 19 GB(97 % 满),长不上去
|
||
大户:Chrome ≈4 GB、Hermes ≈3.7 GB(3 个进程)、upscayl 1.17 GB、whisper-server 0.83 GB、Discord 0.76 GB
|
||
```
|
||
|
||
`copy_*` / `_sha_remote` 当时只 `except OSError`,**而 `MemoryError` 不是 OSError** → 直接冒到
|
||
per-item 兜底;删半截 `remote_tmp` 的清理写在 `except OSError` 里 → **半截/完整的 `.new.mp4` 留在共享盘上**。
|
||
|
||
已修(本轮,`scripts/library_run.py`):
|
||
1. `_sha_remote` / `copy_remote_to_local` / `copy_local_to_remote` 的 `except OSError` → `except (OSError, MemoryError)`(瞬时抖动变重试,不是 FAIL);
|
||
2. `copy_local_to_remote` 内的清理、读回不符时的清理同样放宽;
|
||
3. **per-item 兜底加了无条件删除 `<dest>.new.mp4`**(用 `os.remove` + 忽略 `FileNotFoundError`,**不用** `os.path.exists`,因为该共享盘的元数据可能是陈旧的)。
|
||
|
||
实测(注入 MemoryError):上传路径不再抛异常、半截文件被删;读源路径重试后抛干净的 `IOError`;
|
||
`_sha_remote` 重试 2 次后抛出。
|
||
|
||
**内存仍是硬前提**:物理可用 0 GB / 提交 96.5 % 时重启,第一项照样会 FAIL(现在至少不留残留)。
|
||
重启前建议:关掉 upscayl / 多余 Chrome 标签 / Discord 腾出 5-6 GB;pagefile 上限别放在 97 % 满的 D:。
|
||
|
||
## 8 个 `.new.mp4` 残留(2026-10-09 11:35 逐个探过)
|
||
|
||
原档都在,没丢东西。7 个是**半截前缀**(内容早停 → 没用,删掉让下一轮重做);
|
||
**只有一个是完整的、可以救回来**:
|
||
|
||
| 文件 | 暂存 | 判定 |
|
||
|---|---|---|
|
||
| 2022年/0115《崇祯传》 | 493 MB | ✅ **完整**(3616.8 s 与源一致、内容到 3609 s)→ 校验后可改名安装 |
|
||
| 0622人生海海 / » 0819 李白 / 0527 洪业 / 0807东京传 / 20190601被管教的勇气 / 1230 不安的哲学 / 0511原生家庭 | 8 MB–165 MB | ❌ 半截,删 |
|
||
|
||
## 红线(用户明确要求过)
|
||
|
||
- **绝不删除/覆盖快照原档**(`#snapshot` 是唯一的干净档案)。
|
||
- 失败时**不动 live 文件**;只有"结构 + 全解码 + 图片闸门 + 上传回读 + 安装回读"全过才替换。
|
||
- 杀进程**只按 PID**(绝不用进程名批量杀,`taskkill /IM ffmpeg.exe` 会连带杀掉别人的编码);一档一个锁;同一档不要跑两个实例。
|
||
- 动 GPU(更新驱动/跑 ComfyUI/加载大模型)前先暂停 nvenc 档。
|