diff --git a/SKILL.md b/SKILL.md index 720e6c4..2c77cde 100644 --- a/SKILL.md +++ b/SKILL.md @@ -23,6 +23,25 @@ python scripts/transcode.py "D:/video" --recursive --dry-run # plan only python scripts/transcode.py "D:/video" --out D:/converted ``` +## Scope — read this before applying the size policy + +This policy is for **H.264 (and older MPEG-4/DivX) sources**, where the source is fat enough that +HEVC at matched quality lands 25-50% smaller (the course-library case: 1080p H.264 at +550-2400 kbps, mp3 audio, files 300 MB-1.3 GB). + +**Do NOT run the size hunt on YouTube-sourced libraries (VP9/AV1).** YouTube already serves those +at a near-optimal bitrate for their quality, so re-encoding an already-lean VP9/AV1 file can only +grow it or lose quality - "the source is already as small as it gets, and that is accepted". The +job there is narrower and different: + +| | H.264 library (this policy) | YouTube VP9/AV1 library | +|---|---|---| +| goal | same look, **smaller** file | same look, same size - no shrink sought | +| decode | any correct decoder | **never `*_cuvid`** (wrong frames, see below) | +| quality gate | SSIM >= 0.996 + size ceiling | **per-frame content check vs the software decode** | +| source for a repair | the file itself | **the pre-conversion snapshot backup**, never the already-converted file | +| frame cadence | `-fps_mode cfr` | `-fps_mode cfr` | + ## What "playable everywhere" actually means The intersection that both platforms decode **without a third-party player**: @@ -142,6 +161,99 @@ python /path/to/scripts/transcode.py "Z:/docker/tubesync/download" --recursive \ For a TubeSync library specifically, prefer its own DB-driven pipeline (`tubesync-selfhosted`) — it also updates the database rows. Use this tool for anything outside that library. +## Whole-library runs on a network share (in-place, safe) + +`transcode.py` writes `dst + ".part.mp4"` **next to the source**. Over SMB/WebDAV/Dokan that +is the failure mode to avoid, and when the source is already `.mp4` then `dst == src`, so the +only copy of the file is the one being written. For a library run, drive ffmpeg directly +from a small runner instead: + +1. copy the source **off the share** to local disk (chunked; treat a short read as failure) +2. encode locally, with the same mandatory flags (`-fps_mode cfr`, `-pix_fmt yuv420p`) +3. run the structural gate **and** a full decode pass on the local output +4. upload to `.new.mp4` (never the final name) +5. read the share copy back end-to-end and compare **SHA-256 + byte count** with the local file +6. only then `os.replace()` it over the original, and re-probe the result on the share + +Step 5 costs one extra network read (~1 h per 330 GiB) and is the only thing that actually +proves the remote copy is complete — `getsize()` cannot, and neither can a duration probe: +with `+faststart` the `moov` is written first, so a truncated file still reports full duration. +Keep a JSONL ledger and a per-file skip test ("already HEVC hvc1") so a reboot resumes safely. +A single instance only; the lock matters even more here because two runs would race on `dst`. + +### `plan_for()` gaps to know before a library run + +- **`mpeg4` (DivX/Xvid) returns `None` = "unsupported video codec"** — it is skipped, not + converted, so `.avi`/`mp4v` files silently keep a codec no iPad can play. Give them an + explicit re-encode. +- **Repairing a defective H.264 timeline returns `x264`, not HEVC** (hardcoded in the H.264 + branch). If the requirement is "everything in HEVC", force it — the recipe is a policy, not + the tool's default. +- A truncated file (`moov atom not found`) is a **re-download**, not a transcode job; report it + instead of retrying. + +### Measured throughput (RTX 3060, hevc_nvenc p6, `-tune hq`, cq 27) + +Use these to quote an ETA before starting — the estimate is the user's decision input. + +| source | speed | +|---|---| +| 1080p25 H.264 | **6.4x realtime** (~160 fps) | +| 480x270 | ~34x realtime | +| per-file overhead | ~45 s (local copy + decode pass + upload + read-back hash) | + +A 520 h / 324 GiB library is therefore ~73 h of wall clock, not the ~40 h a bitrate-based +guess suggests. Encode order is a real choice: newest-first finishes the currently-watched +content in hours instead of days. + +## "Same quality" is not the same cq number (measured) + +`hevc_nvenc -cq 27` and `libx265 -crf 27` are **not the same quality**. Same number, +different scale: on one 1080p25 source (source video 1.20 Mbps), 8-minute window, video only: + +| setting | Mbps | % of source video | SSIM | +|---|---|---|---| +| source H.264 (x264 2-pass) | 1.20 | 100% | ref | +| hevc_nvenc cq27 | 1.31 | **109%** | 0.9971 | +| hevc_nvenc cq30 | 0.91 | 76% | 0.9963 | +| libx265 crf24 | 0.83 | 69.5% | 0.9963 | +| hevc_nvenc cq32 | 0.69 | 58% | 0.9955 | +| libx265 crf27 | 0.58 | 48% | 0.9952 | +| hevc_nvenc cq34 | 0.54 | 45% | 0.9947 | + +**Compare only at matched SSIM, never at matched cq/crf numbers.** Matched pairs here: +NVENC cq30 vs x265 crf24 (both 0.9963) -> x265 9% smaller; NVENC cq32 vs x265 crf27 +(0.9955/0.9952) -> x265 16% smaller. So software is **9-16% more bit-efficient at equal +quality**, not the 2-2.5x that a raw cq27-vs-crf27 comparison implies - most of that gap is a +quality difference. Price: ~3x realtime (x265 fast, 1080p) vs 6.4x (NVENC). + +**Why a library can grow instead of shrink.** On sources that are already efficiently +encoded H.264, NVENC cq27 is a *higher* quality target than the source itself, so the video +comes back at ~109% and the file grows. Quality must be pinned to a number, e.g. "SSIM >= 0.996 +against the source", which is the only self-consistent definition when the source is already +lossy and no master exists to measure against. SSIM >= 0.996 lands the video near 70% of the +source bitrate - that is where the actual saving lives. + +**Classify the source before choosing a setting.** bpp = bitrate / (w * h * fps) measures how +much fat is left, and it is readable in milliseconds: + +| bpp | meaning | +|---|---| +| < 0.015 | already soft - re-encoding gains almost nothing, prefer `-c copy` | +| 0.015-0.025 | lean (typical streaming) | +| 0.025-0.040 | normal | +| > 0.040 | real headroom - worth a real re-encode | + +One library measured 131 files < 0.025 against 243 files > 0.040 out of 572 - a single global +cq cannot be right for both. Calibrate per class (1-2 representatives, 60-120 s sample, sweep 2-3 +cq values, keep the cheapest that hits the SSIM target); per-file calibration costs 1-2 min each. +A `-maxrate` of 1.0x the source bitrate is a cheap hard guarantee that output never grows. + +**Audio must match the source, not a fixed 256k.** A weekly series shipping mp3 at 128 kbps was +re-encoded to `-b:a 256k`: that added ~128 kbps for no audible gain, ~+10% file size on a +1.3 Mbps file - enough to cancel the video saving on its own. AAC is more efficient than mp3, so +`-b:a` = the source's own audio bitrate (clamped, e.g. 96-256k) is already a quality upgrade. + ## Pitfalls - **Never write the encode output directly to a network share.** A streaming `.part` write over @@ -163,6 +275,25 @@ For a TubeSync library specifically, prefer its own DB-driven pipeline (`tubesyn bash cannot hand a Chinese filename to native `ffmpeg.exe` even when `ls` shows it. - **`-hwaccel cuda` alone may still software-decode VP9**; `-c:v vp9_cuvid` forces NVDEC. +## The owner's standing requirements — read this first + +When the ask is "convert this library to HEVC, same look, same sound, playable on iPad+Android, +never bigger, no judder", the defaults here are **not** enough: a fixed `-cq 27` targets a quality +*above* an already-efficient H.264 source, so the library grows instead of shrinking. + +| requirement | the answer | +|---|---| +| never bigger than the source | `-maxrate` = 1.0x the source **video** bitrate + a post-encode size assertion | +| same picture quality | pin it to a number: **SSIM >= 0.996** vs the source (video lands near 70% of the source bitrate) | +| same sound quality | copy AAC; for mp3/opus, AAC at **the source's own bitrate**, never a fixed 256k | +| iPad + Android | MP4 + `hvc1` + `yuv420p` + AAC | +| no judder | `-fps_mode cfr` on every re-encode, the three cadence defects gated, and **no `*_cuvid` decoders** | +| verification | mandatory, and it must assert on the output, not on the input | +| CPU encoding | allowed, but **tell the owner first** and quote the wall clock (~3x realtime vs 6.4x) | + +Full policy, including the measured numbers, the source-classification table, the judder detector +table and every CPU/threading knob: **`references/hevc-conversion-policy.md`**. + ## Support files - `scripts/transcode.py` — the tool. `--dry-run`, `--codec`, `--cq/--crf`, `--audio`, @@ -171,3 +302,6 @@ For a TubeSync library specifically, prefer its own DB-driven pipeline (`tubesyn the HEVC level caveat, and what breaks on each device. - `references/verification-and-automation.md` — the defect classes, the structural gate, the browser arbiter and its blind spot, and the 2x2 causality proof. +- `references/hevc-conversion-policy.md` — the owner's standing requirements for a HEVC library job + (size ceiling, SSIM-pinned quality, bpp classification, audio matched to the source, the three + judder classes, the mandatory verification chain, and the CPU/throttling knobs, efficiency first). diff --git a/references/hevc-conversion-policy.md b/references/hevc-conversion-policy.md new file mode 100644 index 0000000..e808fd0 --- /dev/null +++ b/references/hevc-conversion-policy.md @@ -0,0 +1,271 @@ +# HEVC conversion policy — never bigger, same look, no judder + +Owner's standing requirements for a HEVC conversion. Read this before starting a library job; +the default settings in `transcode.py` do **not** satisfy them. + +## 1. Size: shrink where there is headroom, growth is acceptable where there is not + +**Playability on iPad + Android is the primary goal. Size is secondary and conditional** — it is +NOT a universal "must never grow" rule. Two regimes, and they need opposite handling: + +| regime | detector | expectation | `-maxrate` | output assertion | +|---|---|---|---|---| +| **headroom** — an ordinary H.264 encode that is not yet at the transparency floor | bpp > ~0.030, or a sampled pre-flight encode needs **fewer** bits than the source | output **should be smaller**. If it grows, that is a **recipe bug**, not an acceptable outcome | 1.0x the source **video** bitrate | `out <= src` (hard) | +| **already compressed** — YouTube / streaming downloads, near-optimal 2-pass sources | bpp < ~0.030, or a sampled pre-flight encode needs **>=** the source bits | growth is **acceptable**. The file already sits at the transparency floor; only playability matters | 1.5x the source video bitrate | `out <= 1.25x src` (runaway-bloat guard only) | + +The authority is the **sampled pre-flight** (section 2): encode a 60-120 s window at the target +quality and compare bits. bpp is just the cheap prior that tells you which way you expect it to go. + +**Do not clamp to 1.0x on an already-compressed source.** Capping at the source bitrate forces the +encoder *below* the transparency point, which is a visible quality loss **on top of** the +second-generation loss — strictly worse than accepting a slightly larger file. A hard cap is only +correct in the headroom regime, where shrinking is the actual goal. + +Why an already-compressed source can legitimately grow: HEVC's ~40% efficiency advantage is +measured against H.264 at *equal encoding effort*. A near-optimal 2-pass AVC stream is already +closer to the floor than a single-pass hardware HEVC encode is, so at matched perceived quality +there may simply be no bits left to win. + +### An H.264 source with headroom is expected to come out smaller + +When the source is ordinary H.264 with real headroom and the output still grows, the recipe is +wrong, not the source. The two measured causes, both in this history: + +- a quality target **above the source** (`-cq 27` on NVENC = SSIM 0.9971 while the source is + already at ~0.9963-equivalent), landing the video at 109% of the source bitrate; +- audio re-encoded to a **fixed 256k** when the source ships mp3 at 128k: ~+10% for no audible gain. + +### HEVC is not what makes a file playable + +H.264 + AAC in MP4 **already plays on both iPad and Android** — no conversion needed. HEVC is only +*required* for sources that are not already in that shape: Matroska, Opus/Vorbis audio, DivX/Xvid, +MPEG-TS, or a broken frame cadence. For an already-playable, already-lean H.264 file the only +benefit is size; where there is no headroom a re-encode buys nothing and costs a generation, so +`-c copy` (or leaving it alone) is the better answer. Say this to the owner before spending days of +GPU/CPU on files that are already fine. + +The two derived values every rule below needs, from one ffprobe of the source: + +```python +src_video_bps = format_bit_rate - audio_stream_bit_rate # NOT the file's total bitrate +src_audio_kbps = max(96, min(256, audio_stream_bit_rate // 1000)) +``` + +Comparing against the file's **total** bitrate instead of the video's is the specific mistake to +avoid: it turned a real 109% growth into a false 86% shrink in one analysis. + +## 2. Same picture quality — pin it to a number + +"Same quality" is meaningless unless it is measurable, and with an already-lossy source and no +master, the only self-consistent definition is **SSIM against the source**. + +- Target **SSIM >= 0.996** on a sampled window. Measured on 1080p25 H.264: this lands the video + near **70% of the source bitrate** — that is where the saving is. `cq 27` on NVENC gives 0.9971 + and *grows*; `cq 30` gives 0.9963 at 76%; `cq 32` gives 0.9955 at 58%. +- **Never compare cq/crf numbers across encoders.** `-cq 27` (NVENC) = SSIM 0.9971 while + `-crf 27` (x265) = 0.9952. Match on SSIM, then compare bitrate. +- Software x265 is **9-16% more bit-efficient at matched SSIM**, not 2x — most of the raw + difference between cq27 and crf27 is a quality difference, not an efficiency difference. +- Verify the claim per file (or per class): encode a 60-120 s window, measure SSIM vs the same + source window, and fail the item if it is under target. + +## 3. Classify the source before choosing a setting + +`bpp = video_bitrate / (w * h * fps)`, readable in milliseconds, tells you how much fat is left: + +| bpp | verdict | +|---|---| +| < 0.015 | already soft — re-encoding gains nothing, use `-c copy` | +| 0.015-0.025 | lean | +| 0.025-0.040 | normal | +| > 0.040 | real headroom — worth a real re-encode | + +One library: 131 files < 0.025 vs 243 files > 0.040 out of 572. A single global quality setting +cannot be right for both — that is precisely how a job ends up growing some files and shrinking +others. Calibrate per class on 1-2 representatives (60-120 s, sweep 2-3 quality levels). + +## 4. Same sound quality + +- Source audio already **AAC** -> `-c:a copy`, bit-identical. Always preferable. +- Source audio **mp3/opus/vorbis** -> must be re-encoded; those are not valid in MP4. +- **`-b:a` must track the source, never a fixed 256k.** A weekly series shipping mp3 at 128 kbps + re-encoded to 256k AAC added ~128 kbps for zero audible gain: ~+10% file size, enough to cancel + the video saving by itself. AAC is more efficient than mp3, so **matching** the source bitrate is + already an upgrade. Clamp to a sane floor (96k). + +## 5. Playable on iPad AND Android + +MP4 + `-tag:v hvc1` (Apple rejects `hev1`) + `-pix_fmt yuv420p` + AAC. No Matroska, never Opus. +HEVC 1080p is Main profile Level 4.0 — every modern iPhone/iPad (A9+) and Android 5+ handles it, +but a cheap/old Android device is only guaranteed Main Level 3 (~SD). + +## 6. No judder — three defect classes, same symptom + +`-fps_mode cfr` is mandatory on every re-encode: with the default `auto`, ffmpeg **drops** frames +it cannot place (0.5-1.3% measured), leaving doubled frame intervals that play as judder. CFR +duplicates a frame instead, which costs almost nothing. Never pass `-r` unless you want a +different playing speed. + +| class | signature | detector | fix | +|---|---|---|---| +| source-inherited VFR | `r_frame_rate != avg_frame_rate` | ffprobe header, gap > 0.01 | re-encode (remux cannot repair) | +| encoder-dropped | doubled intervals (0.040 + 0.080) | histogram `packet=duration_time` shows 2+ values | `-fps_mode cfr` | +| **flat composition offsets** | `r == avg` **exactly**, uniform `stts`, clean decode, still drops ~23% | `ctts` entry count <= 1 on a B-frame stream, or 0 PTS inversions over 60 s | real re-encode; **no lossless repair exists** | + +The third class is the trap: every cadence test passes and the file still stutters on real +hardware. Gate it explicitly (see `scripts/transcode.py`: `_ctts_entries()`, `pts_inversions()`). + +**Never decode with a `*_cuvid` hardware decoder.** Measured on this hardware (RTX 3060, +ffmpeg 9.0.1): `av1_cuvid` emitted **the wrong picture on 1.8% of frames** (a frame from a +different moment, not a corrupt one). The container stayed perfect — exact intervals, correct +`ctts`, clean decode — so every structural check passed while the output visibly jumped. Use +software decode (4K AV1 ran ~150 fps on the i9-13900K) or the modern +`-hwaccel cuda -hwaccel_output_format cuda` path, and if you ever must use cuvid, compare its +frames against the software decode with per-frame SSIM (>=0.99 everywhere). + +**Judge the picture, not the container, when the complaint is "it jumps".** + +## 7. Verification — yes, mandatory, and it must assert on the output + +Per item, in this order: + +1. **Local staging.** Fetch the source off the share first; never encode over SMB/WebDAV/Dokan. +2. **Structural gate** on the encode: codec/tag/duration/resolution/pixel format, `r` vs `avg` + within 0.01, `ctts`/PTS inversion check, one distinct frame duration. +3. **Full decode pass** (`ffmpeg -v error -i OUT -f null -`) — the only check that proves the + payload is not truncated or corrupt. +4. **SSIM vs the source window >= target** (section 2). +5. **Size assertion**: output video bitrate <= source (section 1). +6. **Upload to a temp name, read the share copy back end-to-end, compare SHA-256 + byte count**, + then replace atomically. A duration probe is not enough: with `+faststart` the `moov` is written + first, so a truncated upload still reports the full duration. +7. **Re-probe on the share** after the replace. + +Cheap confidence check on a healthy file, one distinct value expected: + +```sh +ffprobe -v error -select_streams v:0 -read_intervals "%+60" \ + -show_entries packet=duration_time -of csv=p=0 FILE | sort -u | wc -l +``` + +**Do not hand-sample videos before converting** — nothing here is spec-dependent, so the guarantee +belongs in the output assertion. Chrome cannot arbitrate HEVC either: `canPlayType('...hvc1...')` +returns `''`, so an HEVC file reports 0 decoded / 0 dropped and looks perfect while nothing played. +HEVC smoothness is confirmable only on the Apple device, via the frame-duration histogram, or on +an Android player. For an owner-facing A/B, serve the original and the variant side by side in a +real browser and let them compare (the H.264-only variant is the one Chrome can measure). + +## 8. CPU (software) encoding — every knob, and what each one costs + +**Remind the owner before starting a CPU job and quote the wall-clock time.** Software x265 runs +3.5-4.4x realtime at 1080p depending on cores and preset (vs 6.4x for `hevc_nvenc`), so a 520 h +library is days of CPU. Say so before starting, not after — see the measured table in the topology +section and the tier timings in section 9. + +Efficiency first, and the knobs do different jobs: + +| goal | knob | effect | +|---|---|---| +| better bits-per-quality | `-preset medium|slow|slower` | **the only lever that buys efficiency**; each step ~5-8% smaller at equal quality, ~1.5-2x slower | +| fewer cores | `-x265-params pools=N` (N threads) — for `libx265`, ffmpeg's `-threads N` maps to frame threads | fewer cores, roughly linear slowdown, **no efficiency gain** | +| fewer frame threads | `-x265-params frame-threads=2` | less parallelism, marginal efficiency *gain*, slower | +| CPU affinity | `start /affinity ffmpeg ...`, or PowerShell `(Get-Process ffmpeg).ProcessorAffinity` | pins to chosen cores; keeps the machine responsive | +| low priority | PowerShell `(Get-Process ffmpeg).PriorityClass='BelowNormal'` | yields to everything else at no efficiency cost | +| **no high frequency** | `powercfg /setacvalueindex SCHEME_CURRENT SUB_PROCESSOR PROCTHROTTLEMAX 60` then `powercfg /setactive SCHEME_CURRENT` | caps clocks: best perf/watt, quietest, slowest — match `pools` to the capped cores | + +**Preset buys efficiency; thread count does not.** If the priority is efficiency rather than speed, +prefer a slower preset at a lower core count over a faster preset with all cores. Never run two +encoders of the same kind at once: they compete for the same unit and drop frames — the exact +defect the gates exist for. One CPU encoder running *alongside* one GPU encoder is fine (different +units), see section 9. + +### This host's measured topology and the P-core-avoidance recipe + +Windows reports the i9-13900K VM as "13 cores / 26 logical" — a misreport of a heterogeneous +topology. A per-logical-CPU microbenchmark settles it in 30 seconds: + +| logical CPU | relative single-thread throughput | +|---|---| +| **0-15** (8 P-cores x HT) | 0.43-0.52 s for a fixed workload | +| **16-25** (10 E-cores) | 0.79-0.91 s — **56% of a P-core** | + +Pin to the E-cores only (best perf/watt, keeps the P-cores free for the desktop): + +```sh +# affinity mask = bits 16-25 set = 0x3FF0000 (= 67,043,328) +start /affinity 3FF0000 ffmpeg ... # cmd +``` +or set the mask on the driving Python process (children inherit it): + +```python +ctypes.windll.kernel32.SetProcessAffinityMask( + ctypes.windll.kernel32.GetCurrentProcess(), ctypes.c_size_t(0x3FF0000)) +``` + +Match the thread count to the mask (`-x265-params pools=10`). **`pools` does not choose cores — +the affinity mask does**; `pools` only caps threads. + +Measured cost of E-core-only encoding (x265 preset medium, 1080p25, 120 s sample): + +| cores | speed | output | +|---|---|---| +| all 26 threads | 4.37x realtime | 0.76 Mbps | +| **E-cores only (10 threads)** | **3.51x realtime** | 0.76 Mbps | + +Only **20% slower**, not the ~3x that per-core throughput suggests: the 10 E-cores are real cores +while half of the P-core threads are SMT siblings, and x265 does not scale linearly anyway. So +E-cores-only is the right default when the priority is efficiency over speed. + +## 9. CPU and GPU tiers in parallel + +Different tiers can encode **at the same time** — NVENC is a fixed-function ASIC, x265 is CPU. +Pair an E-core-pinned CPU job with a GPU job and the P-cores stay free for the user. Wall clock +becomes `max(cpu_tier, gpu_tier)` instead of the sum: + +| tier | files | GiB | content h | recipe | time | +|---|---|---|---|---|---| +| fat (bpp > 0.040) | 252 | 153 | 219 | x265 crf24, E-cores | 65 h | +| mid (0.025-0.040) | 198 | 117 | 183 | hevc_nvenc cq30 | 31 h | +| lean (< 0.025) | 131 | 54 | 118 | `-c copy` + audio match | <1 h | + +serial 96 h (4.0 days) -> parallel **66 h (2.7 days)**. Requirements to run it safely: + +- **a lock per tier**, not one global lock, and a separate local staging dir per instance; +- the share is the shared resource: two instances double the WebDAV/Dokan traffic (staging read + + upload + read-back hash). Test a small batch in parallel before committing the library; +- run the fast `-c copy` tier on its own first, so it is not contending while the two big jobs settle; +- `-c copy` only where the timing gate passes — a file in the flat-ctts class must be re-encoded + whatever its bpp says. + +## 10. The three recipes, ready to run + +`SRC` = the local staged source, `SRC_V` = `src_video_bps` from section 1, `A` = `src_audio_kbps` +(section 4). Affinitize the process to the E-cores (`0x3FF0000`) before the tier-C command. + +```sh +# ---- tier A: lean (bpp < 0.025) — no video re-encode at all, size cannot change +ffmpeg -y -i SRC -map 0:v:0 -map 0:a:0? -map_metadata 0 \ + -c:v copy -c:a copy -movflags +faststart OUT.mp4 # -c:a aac -b:a A when the source is mp3 + +# ---- tier B: mid (0.025-0.040) — GPU, quality pinned to SSIM >= 0.996 +ffmpeg -y -i SRC -map 0:v:0 -map 0:a:0? -map_metadata 0 \ + -c:v hevc_nvenc -preset p6 -tune hq -rc vbr -cq 30 -b:v 0 \ + -maxrate SRC_V -bufsize <2.5x SRC_V> \ + -profile:v main -pix_fmt yuv420p -tag:v hvc1 -fps_mode cfr \ + -c:a copy -movflags +faststart OUT.mp4 # -c:a aac -b:a A when the source is mp3 + +# ---- tier C: fat (bpp > 0.040) — CPU, E-cores only, efficiency first +ffmpeg -y -i SRC -map 0:v:0 -map 0:a:0? -map_metadata 0 \ + -c:v libx265 -preset medium -crf 24 -x265-params pools=10 \ + -maxrate SRC_V -bufsize <2.5x SRC_V> \ + -pix_fmt yuv420p -tag:v hvc1 -fps_mode cfr \ + -c:a copy -movflags +faststart OUT.mp4 +``` + +Why these exact numbers: `cq 30` (NVENC) and `crf 24` (x265) both land near **SSIM 0.9963** — the +matched-quality point where the video comes back at ~70-76% of the source bitrate. Neither is +`cq 27`/`crf 27`: those are different qualities from each other, and `cq 27` is *above* an +already-efficient source, which is what made a library grow. + +Every tier still runs the whole verification chain from section 7 — tier A included: a `-c copy` +can still originate a truncation, and a flat-ctts source cannot be copied at all.