hoelee 24892668a9 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.
2026-10-07 10:13:15 +08:00
2026-10-04 14:56:20 +08:00
2026-10-04 14:56:20 +08:00

universal-video-transcode

Any video or audio file in, one MP4 out that plays on both an iPad and an Android device — at the best quality the target codec allows.

Give it a path. A file converts; a folder converts everything in it.

python scripts/transcode.py "D:/video/clip.mkv"
python scripts/transcode.py "D:/video" --recursive --dry-run    # plan only, no encoding
python scripts/transcode.py "D:/video" --out D:/converted

What it does

It reads the source file (never the filename) and picks one of three recipes:

recipe when video audio
copy H.264/HEVC with a usable timeline -c copy — bit-identical copy if AAC
fix-audio same, but the audio is not AAC -c copy AAC 256k
encode VP9/AV1, or a timeline that cannot be copied HEVC (or x264) copy if AAC, else AAC 256k

A copy is used whenever a copy is possible, because a copy is lossless by definition — the strongest possible answer to "keep the quality the same".

Then it verifies the output and refuses to accept it otherwise: duration, resolution, codec, hvc1 tag, 8-bit pixel format, uniform frame cadence, no flat composition offsets, and a full decode pass.

Why it exists

A file can drop ~23% of its frames in a browser while r_frame_rate == avg_frame_rate reads exactly equal — so every fps-based check calls it healthy. The cause is deterministic and header-readable: the MP4's composition-offset table is unusable, either a single constant ctts entry (PTS == DTS + const) or no ctts box at all (PTS == DTS). The timestamps are then in decode order, and the decoder's reordered output frames carry the wrong times.

This repo gates on that, structurally, without playing anything.

python scripts/selftest.py     # builds both defect shapes and asserts the detector
fixture                            expect   got      verdict
src.mp4                            False    False    ok    healthy MP4, proper ctts
src.mkv                            False    False    ok    Matroska - no ctts by design
flat_noctts.mp4                    True     True     ok    MP4 with NO ctts box
flat_ctts1.mp4                     True     True     ok    MP4 with a single constant ctts entry

Measured on the file that prompted this: 23.0% of frames dropped before, 0.0% after.

The full benefit case — the ten real failures this pipeline hit and the guard each one produced — is in docs/WHY.md.

Compatibility

iPad / iOS Android
Container MP4/MOV only — no Matroska MP4 and MKV both fine
Video H.264 (all), HEVC hvc1 (A9+) H.264 Baseline mandatory; HEVC 5.0+
Audio AAC — never Opus AAC and Opus both fine

Apple is the constraint, not Android: iOS has no Matroska demuxer and cannot play Opus, so MP4 + AAC is what forces the conversion. Two traps: HEVC must be tagged hvc1 (ffmpeg defaults to hev1, which Apple refuses), and Android only guarantees HEVC Main profile Level 3 — use --codec x264 when cheap/old Android hardware is in scope.

Details and citations: references/compatibility-matrix.md.

Quality policy

  • Never upscale — the source resolution is preserved; there is no scale in the pipeline.
  • HEVC at -cq 27: measured SSIM 0.9937 (visually transparent) at ~source size.
  • H.264 (when chosen for reach) at -crf 20.
  • Audio copied when already AAC; otherwise AAC 256k, where the residual error is the source's own lossy error — 320k buys nothing measurable.
  • -fps_mode cfr on every re-encode: without it the encoder silently drops 0.5–1.3% of frames, leaving doubled intervals that play as judder.

Running it unattended

Safe to schedule — it is idempotent and self-locking:

  • a lock file refuses a second instance (two concurrent NVENC jobs starve one encoder and drop frames, the exact defect this tool removes)
  • an already-playable file is skipped, so a re-run costs one probe
  • a failed item leaves the input untouched, so the next run retries it
  • aborts below 5 GB free rather than filling the work drive
python scripts/transcode.py "Z:/media/incoming" --recursive \
  --out "Z:/media/converted" >> /var/log/transcode.log 2>&1

Install as a Hermes skill

This repo is the canonical source. Copy it into the skill directory rather than editing the skill copy in place:

cp -r . "$LOCALAPPDATA/hermes/skills/media/universal-video-transcode"

See AGENTS.md for the non-negotiables.

Options

flag meaning
--dry-run print the plan, encode nothing
--codec auto|hevc|x264 force the video codec (x264 for maximum reach)
--cq N / --crf N quality (HEVC default 27 / H.264 default 20)
--audio 256k, --channels 0 audio bitrate; 0 keeps the source layout
--cpu force libx265/libx264 (no NVENC)
--keep keep the input file
--overwrite replace an existing output
--no-decode-check skip the full decode pass (faster, less safe)
--lock PATH lock file location

ffmpeg/ffprobe are taken from PATH, or from TRANSCODE_FFMPEG / TRANSCODE_FFPROBE.

License

MIT © 2026 Lee Teong Hoe (Hoelee Enterprise)

S
Description
Any video/audio in, one MP4 out that plays on iPad AND Android. Source-file-driven recipe selection (lossless copy when possible), HEVC/H.264 + AAC, and a header-only gate that catches the frame-timing defect no fps test can see. Ships as a Hermes skill.
Readme MIT
875 KiB
Languages
Python 94.2%
Shell 5.8%