gate: decode both sides sequentially; lock: heartbeat + age-based staleness

Two bugs the library run exposed on real files:

* An input seek is not a faithful decode. On an H.264 source with open GOPs the frames after the
  landing point decode differently from a sequential decode - the SAME file compared with itself,
  once seeked and once decoded in order, scored min 0.25 / mean 0.82, and the gate condemned good
  encodes because of it. Both sides are now decoded sequentially with the window cut by the trim
  filter, which is also the only decode guaranteed to match what the encoder itself saw.
* Staleness of the run lock. Killing a background session kills the wrapper but not the python
  underneath, so a live run's lock survives and the next start is correctly refused (it looked like
  a pid-reuse bug; it was not). The lock now carries a heartbeat, staleness is decided by age as
  well as pid, and a takeover that cannot delete a file the OS still holds overwrites it in place.
This commit is contained in:
2026-10-07 10:13:15 +08:00
parent 1fd975fbb4
commit 24892668a9
3 changed files with 54 additions and 16 deletions
+2
View File
@@ -261,6 +261,8 @@ re-encoded to `-b:a 256k`: that added ~128 kbps for no audible gain, ~+10% file
- **Never build a verification harness on a stream-copy cut.** `-ss X -c copy` on an H.264 file with B-frames loses packets (measured: a 60 s cut of a 25 fps source yielded 1270 frames instead of 1500, with the content shifted after each dropped packet), so an encode of that cut can never line up with anything and the comparison reports a fake SSIM 0.25 minimum. Compare whole files (or byte-copies of them): a byte copy is faithful, a stream copy is not. Corollary - before believing any picture verdict, make the harness prove itself on a file against ITSELF (must be lo=1.0).
- **Time-box a single-file mystery.** When one file resists diagnosis after a couple of experiments, park it (leave it untouched, log it as 'gate could not verify'), report it, and move on - a library run must not stall on one item. Revisit parked files in a batch later.
- **Killing a background session may not kill the process.** The session is a wrapper around the command; killing the session left the real python alive holding its lock, so the next start was refused by a lock that was telling the TRUTH while the log looked like a stale-lock bug. Check the real PID before concluding (`powershell -NoProfile -Command "Get-CimInstance Win32_Process -Filter 'ProcessId=<N>' | Select ProcessId,ParentProcessId,CreationDate,CommandLine"`) and kill that PID. Corollary, now implemented in `Lock`: the lock file is touched by a heartbeat every minute and staleness is decided by age (5 min) as well as pid, and a takeover that cannot remove a file still held by the OS overwrites it in place instead of crashing.
## Pitfalls
- **A RaiDrive/Dokan write can return with NO error while the file is a fraction of what it should be.** Measured: a 403 MB upload left 12.5 MB on the share, and RaiDrive's own "the operation didn't complete within specific time limit" toast arrived *after* the write had already returned success. Never trust a write to a Dokan-backed share: hash the local file, hash the share copy back, compare size AND SHA-256, and only then rename. On a mismatch, retry the upload (it usually works on the second attempt) rather than failing the item. Uploads should use small chunks (4 MB) with a periodic `flush`+`fsync`; a partial `*.new.mp4`/`*.part.mp4` must be deleted, never resumed from `os.path.getsize()` on the share (RaiDrive reports stale sizes, so a resume offset taken from it corrupts the file).