Back to the catalog

av1repack documentation

Bundle OKF 0.2 · 2 conceitos · sugi/av1repack

Open source Repository Open in the app JSON README (API)

About

# av1repack documentation

Why the tool behaves the way it does, and the measurements that decided it.

The two directories carry different kinds of thing and have different lifetimes.
A decision can be superseded; the measurement it rested on stays true. So
decisions cite measurements, and measurements never mention decisions.

* [Architecture Decision Records](adr/) - what was decided, and what the
alternatives were
* [Measurements](measurements/) - dated findings with the software versions they
were taken against

Project vocabulary lives in [CONTEXT.md](../CONTEXT.md) at the repository root,
which is a glossary and nothing else.

Details

Kind
OKF bundles
Topic
Files & documents
Publisher
sugi
Origin
okf_github
Category
dados
Version
0.2
Last push
2026-08-19T19:33:31Z
Repository state
ativo
Language
Python
License
MIT
Added
2026-09-08 09:01:12
Updated
2026-09-08 09:01:12
Origin id
sugi/av1repack:docs/index.md

README

# av1repack

非 AV1 の動画を、画質を落とさずに SVT-AV1 へ再エンコードする。

なぜこう動くのかは [`docs/adr/`](docs/adr/)、その根拠になった実測は
[`docs/measurements/`](docs/measurements/)、用語は [`CONTEXT.md`](CONTEXT.md)。

## 使い方

インストールはしない。クローンした場所からそのまま走らせる(理由は
[ADR-0022](docs/adr/0022-run-from-a-checkout.md))。初回は ffmpeg を取ってくるところから
始まる(「ffmpeg」を参照)。あとは走らせるだけで、候補が尽きるまで流し切る。

    python3 -m av1repack --root /path/to/videos \
        --workdir /tmp/av1repack

途中で止めたい、あるいは結果を確認してから続けたいときは `--limit` を付ける。次は
20 本処理した時点で終わる。

    python3 -m av1repack --root /path/to/videos \
        --retire /path/to/videos/_retired \
        --workdir /tmp/av1repack --limit 20

まず `--dry-run` で予測だけ出す。実行を中断しても、同じコマンドを再実行すれば
続きから進む(処理済みは `--state` の記録でスキップされる)。
dry-run の予測記録は処理済みとは扱わないため、次の dry-run または本実行で再評価される。

既定は `--state state.jsonl`、`--workdir work`、`--jobs` はプロセッサ数の 1/4(切り捨て、最低 1)、
`--lp 3`。`--retire` に既定はなく、**指定しなければ原本を上書きする**(復元できない)。指定すれば
そのディレクトリへ原本を移す。各ファイルは 4 区間を各 5 秒測定し、VMAF に 6 スレッドを使う。

`--jobs N` は**同時に処理する本数**で、候補は尽きるまで流し切る。1 本終わればすぐ次が入るので
枠が遊ばない。`--limit M` を付けると、実作業が必要なファイルを M 本処理した時点で止まる
(既定の `0` は無制限)。`--jobs 0` は選抜した全ファイルを同時に走らせる。同時実行数はそのまま
資源の倍率になるので、空き容量とコア数から本数を決める(「注意」を参照)。

`--lp N` はエンコード 1 本に与える SVT-AV1 のスレッド数(`-svtav1-params lp=N`)で、CRF 探索の
サンプルエンコードにも同じ値がかかる。サンプルもジョブ枠を 1 つ占めて走るので、そこだけ
マシン全体を掴ませると他のジョブが探索のあいだ飢える。`--lp 0` は SVT-AV1 の既定(全コア)に
任せる。既定の組み合わせ(`--jobs P/4`、`--lp 3`)はエンコードに論理プロセッサの 3/4 を割り当てる。
プロセッサ数は `sched_getaffinity` から取るので、`taskset` や cgroup で絞った分は尊重される。

**幅を広げるより本数を増やすほうが速い。** SVT-AV1 のスレッド効率は `lp=6` で頭打ちになり、
それ以上は 1 スレッドも仕事をしない(i9-12900T / 24 論理プロセッサでの実測、1 本あたり fps)。

| lp | 2 | 3 | 6 | 12 | 18 | 24 |
|---|---|---|---|---|---|---|
| 1080p preset 4 | 17.3 | 25.1 | 33.0 | 32.5 | 32.3 | 32.3 |
| 4K preset 6 | 7.2 | 10.4 | 13.6 | 13.7 | 13.5 | 13.5 |

18 スレッドの予算で 6 本を流し切るのにかかった時間は、4K で `--jobs 6 --lp 3` が 27.9 秒、
`3/6` が 29.2 秒、`2/9` が 31.3 秒、`1/18` が 43.7 秒。既定が最速で、1 本に全部与える形は
1.6 倍遅い。RAM と `--workdir` が厳しいなら `--jobs 3 --lp 6` が次善で、資源は半分、
スループットの損は 5% 以内(短いクリップでの実測なので、本編エンコードでは広い側がもう少し伸びる)。

選抜の前に、非対応拡張子と「すでに AV1」をサンプリングなしで判定して状態ファイルに記録する。
これらは `--limit` の本数に数えない(判定は 1 本あたり ffprobe 1 回、約 0.1 秒)。VFR や
ストリーム構成の不正は全パケットの走査が必要なので選抜では判定せず、`--limit` を消費してから
`error` になる(エンコードはしない)。選抜は実行開始時に 1 回だけ行うので、実行中に増えた
ファイルは次回の対象になる。

成功した AV1 出力は元ファイルのパスを置き換え、拡張子も保持する。対応する
出力拡張子は `.mp4`、`.m4v`、`.mkv`、`.webm` であり、それぞれに対応するコンテナで書き戻す。
それ以外の候補拡張子は、元ファイルを変更せず `error` として記録する。
`--retire` と `--workdir` が `--root` 配下なら候補から除外し、出力パス情報を持たない旧版の完了記録に
対応する `.m4v` エイリアスだけも除外する。両者を `--root` と同じ場所には指定できない。
画質の下限、80% のサイズ上限、解像度から選ぶ SVT-AV1 preset は固定で、CLI から変更できない。
下限はどの解像度でも同じ 1 組の数値だが、それを測る VMAF のモデルは解像度で変わる(「ffmpeg」を参照)。
数値を据え置いたままモデルを変えたので、94/90 が 4K に要求する画質は意図的に下がっている
(下げ幅は [ADR-0023](docs/adr/0023-score-4k-with-the-4k-vmaf-model.md))。

終了コードは、エラーなしが 0、状態ファイルに `error` があれば 1、不正な引数または存在しない
ディレクトリを `--root` に指定したとき、ディレクトリ走査に失敗したとき、および ffmpeg を
用意できなかったときは 2。

## ffmpeg

PATH の ffmpeg は使わない。[BtbN/FFmpeg-Builds](https://github.com/BtbN/FFmpeg-Builds) の
linux64 gpl 静的ビルドを自分で取ってきて、`vendor/ffmpeg/bin/` に置いたものだけを使う。
資産は `ffmpeg-n<major>.<minor>-latest-linux64-gpl-<major>.<minor>.tar.xz` のうち版が最大のものを選ぶ。
BtbN が新しいリリース系列を出せば、その時点で自動的にそちらへ移る。ffplay と man と presets は
展開せず(436 MB のうち 158 MB)、`ffmpeg` と `ffprobe` の 2 本だけを取り出す。

取得は初回だけで、以降は置いてあるものを使う。更新したいときは `--update-ffmpeg` を付ける。
取り出した直後に `-version` と `libsvtav1` エンコーダ・`libvmaf` フィルタの有無を確かめ、
通ったものだけを所定の位置へ入れ替える。落第すれば入れ替えず、前のビルドがそのまま残る。
導入したビルドの素性は `vendor/ffmpeg/build.json` に、実行ごとのバージョンはログの先頭に出る。

自前のビルドを使いたいときは `--ffmpeg /path/to/ffmpeg`(同じディレクトリの `ffprobe` を使う)。
導入先を変えたいときは `--ffmpeg-dir`。`--ffmpeg` は `--update-ffmpeg` とも `--ffmpeg-dir` とも
併用できない。**PATH へのフォールバックはしない。** 取得も導入もできなければ、どのビルドで
符号化したのかログから追えなくなるので、走らずに終了コード 2 で止まる。

自前で持つ理由はエンコーダの版にある。Debian trixie の SVT-AV1 は 2.3.0 だが、このビルドは
4.2.0 系(`v4.2.0-73-gfb0ed7e59`)で、同じ画質までの所要時間が違う。libvmaf はビルド自身が
JSON ログに名乗るリビジョン `e80d6c5` で、リリース番号は出さない。それでも正規化の前提は
崩れない。天井はファイルごとに実行時に測り直し、天井もスコアも同じビルドで測るからで
([ADR-0016](docs/adr/0016-only-a-managed-ffmpeg-build.md))、現行ビルドで測り直した天井も
[`docs/measurements/vmaf-ceiling.md`](docs/measurements/vmaf-ceiling.md) が記録した幅に収まる
([`docs/measurements/vmaf-model-by-resolution.md`](docs/measurements/vmaf-model-by-resolution.md))。
モデルは解像度で選び(4K は `vmaf_4k_v0.6.1`、それ未満は `vmaf_v0.6.1`。
[ADR-0023](docs/adr/0023-score-4k-with-the-4k-vmaf-model.md))、天井もスコアも同じモデルで測る。
なお preset 6/4 の実測値は SVT-AV1 2.3.0 上のもので、今の 4.2.0 系では測り直していない。
CRF はファイルごとに VMAF ゲートで探索し直すので画質の下限は変わらないが、preset の
費用対効果は再測定していない。

ffmpeg 8 は表示行列(回転)をフレーム側にしか持たないため、再エンコードすると出力に
残らない(7.1 では残った)。回転を持つ入力は、エンコード後にストリームコピーで 1 回だけ
包み直して回転を戻す。`-metadata rotate=` は無視され、ffmpeg に自動回転させると
参照クリップと VMAF が前提にしている符号化ジオメトリが変わってしまう。

## 判定

| 条件 | 値 |
|---|---|
| 画質 | 正規化 VMAF mean ≥ 94.0 かつ 5%tile ≥ 90.0 |
| サイズ | 元の 80% 以下 |

どちらかを満たさなければ書き戻さず、理由を状態ファイルに残す。

音声は 1 ストリームだけ Opus 96k に再エンコードする(コピーしない)。元が AAC で、
コンテナが 100 kbps 未満と申告している場合だけ 64k に落とす。VMAF は動画しか見ないので、
この判定に音声は入らない([ADR-0015](docs/adr/0015-re-encode-audio-to-opus.md))。

### 判定済みの測り直し

VMAF モデルの切り替え([ADR-0023](docs/adr/0023-score-4k-with-the-4k-vmaf-model.md))のような
判定基準の変更は、確定済みの記録には遡らない。`pipeline.process` は dry-run 以外の記録が
あるファイルを再評価せずその記録を返すし、記録には解像度が入っていないので、状態ファイル
だけから 4K の行を選び出すことはできない。手っ取り早いのは、`size_fail` と `quality_fail` の
行をまとめて落とすこと。

    grep -v '"status": "size_fail"' state.jsonl |
      grep -v '"status": "quality_fail"' > state.new.jsonl && mv state.new.jsonl state.jsonl

次の実行で該当ファイルが再評価される。4K 以外も巻き込んで測り直しになる。その 4K 以外の
行は、ツールチェーンが同じなら結果は変わらず探索の時間を食うだけだが、エンコーダの版が
上がっていれば、古い版で確定した判定が動くこともある。絞りたければ、失敗した元ファイルは
書き戻されず記録のパスに残っているので、各パスに `ffprobe` を当てて解像度を見て、4K の行だけ
落とせばよい。選抜と同じく 1 本あたり約 0.1 秒で、失敗した全ファイルの探索をやり直すより
桁違いに安い。

## 進捗表示

FFmpeg の `-progress` を読み、ファイルごと・段階ごとに進捗を出す。約 5% 進むたび、
進捗が動かなくても 10 秒ごとに 1 回更新する。

端末に出すときは、実行中のファイル 1 本につき 1 行を画面下部に固定し、その場で書き換える。
その先頭に実行全体の 1 行を出す。ログ行はその上を流れ、終わったファイルの行は消えて完了ログに変わる。

選抜中(対象を数え上げている間)は全体行だけが出る。

    selecting 1203/5000 candidates 38 to process 0:02:01 elapsed

エンコードが始まると本番の全体行に替わる。

    08:41:02 [1/3] start /path/to/videos/a.mp4     ← ログはここを流れる
    08:41:28 [3/3] .../c.mp4 done crf=52 8.3% of original vmaf=94.11/91.76
    overall 12/57 files 34% 210/620 GB 4 running 1:02:11 elapsed ~2:00:41 left
    a.mp4 encode crf=32 45% 0:12:03/0:26:40 speed=1.2x  ← ここから下が固定ブロック
    b.mp4 vmaf crf=38 chunk 2/7 60% 0:00:12/0:00:20 speed=3.1x

全体の進み具合は**本数ではなくバイト数**で測る。1 本あたりの大きさが桁違いに散らばるので、
本数では現在地を言い当てられない。実行中のファイルは終わるまで 0 と数える。残り時間は
「完了バイト ÷ 経過時間」からの外挿で、最初の 1 本が終わるまでは出さない。出たあとも、
同時実行の枠が埋まり切るまでは経過時間だけが先行するので長めに振れ、完了が溜まるにつれ
収束する。経過時間は実行開始からで、選抜にかけた時間は含めない(含めると残り時間が伸びる)。
ファイル 1 本あたりの固定コスト(参照抽出、5 秒区間 4 本のサンプル、VMAF 天井測定、最大 6 回の
CRF 探索試行)はファイルサイズにほとんど依存しない。サイズに比例するのは本エンコードだけなので、
小さいファイルが集中する区間ではこの固定コストがサイズ項を上回り、残り時間は楽観側に振れる。

段階は `reference`(参照クリップの区間抽出)、`vmaf-ceiling`(天井測定)、
`sample`(CRF 探索の試行エンコード)、`vmaf`(試行の測定)、`encode`(本エンコード)。
VMAF は `チャンク番号/総数` を、CRF 探索は試行中の CRF を出す。時刻は入力メディア上の位置で
あって実時間ではない。CRF 探索の試行回数は二分探索の結果次第なので事前には出せない(上限 6 回)。

行は端末幅で切り詰める(折り返すとカーソルの戻り幅がずれて表示が崩れるため)。実行中の本数が
画面の高さを超えるときは、入りきる分だけ表示して最後の行を `... and N more` にする。

出力をファイルやパイプにリダイレクトしたときは制御文字を出さず、他のログと同じ
タイムスタンプ付きの流れる 1 行として出す。並列実行中は入り混じるので、行頭のファイル名で
読み分ける。全体行は 1 本終わるごとに 1 行ログに出る。

## 注意

- 同時実行数はそのまま資源の倍率になる。`--jobs N` は次のすべてを N 倍にする
  - エンコードスレッド: `--lp` × N(既定 3 なので `--jobs 4` で 12 スレッド)
  - VMAF スレッド: `--threads` × N(既定 6 なので `--jobs 4` で 24 スレッド)
  - RAM: VMAF のチャンク処理が 4K で 1 本あたり約 4.4 GB
  - `--workdir`: 4K の生参照で 1 本あたり 7.5 GB、加えて本エンコードの出力が元サイズぶん
- `--threads` の既定は 6 のままなので、既定の `--jobs`(P/4)だと CRF 探索中は VMAF だけで
  論理プロセッサの 1.5 倍のスレッドが載る。VMAF は CPU 律速ではないので実害は小さいが、探索が
  遅いと感じたら `--threads` を下げる
- `--jobs 0` は選抜した全ファイルを同時に走らせる。数十本あれば確実に破綻するので、
  本数が少ないと分かっているときだけ使う。危険なのは本数ではなく**並列度が無制限になること**
- `--workdir` はメディアと別のボリュームに置く
- ディスクが厳しいときは `--jobs` を小さくする。`--retire` を使うなら `--limit` で区切り、
  退避した原本を確認・削除してから次へ
- `--retire` を指定すると元ファイルは削除せず退避する。**メディアと同じファイルシステムを指定
  すること。** 別 FS だと移動が実コピーになり、原本ぶんの読み書きと容量を余分に食う
- **`--retire` を `--limit` なしで使うと、退避した原本が実行中ずっと積み上がる。** 出力のぶんだけ
  空きが減り続けるので、ライブラリ全体を流し切るには原本と出力の合計が入る空きが要る。
  起動時に警告する
- `--retire` なしは原本をその場で上書きする。取り消せないので、まず `--dry-run` で予測を見て、
  次に `--retire` 付きで数本試してから使う
- 書き戻しは、本エンコードの出力を書き戻し先の隣に `<name>.tmp` として置いてから rename する。
  `--workdir` は普通は別ボリュームなので、バイトが渡るのは `.tmp` への段階まで。最後の rename は
  同一ディレクトリ内なので不可分で、中途半端なファイルが元のパス名に載ることはない
- `.tmp` が残っているのは、退避のあとに rename が失敗した場合だけ。そのときは原本が `--retire`
  にあり、`.tmp` が完成した AV1 なので、手作業で入れ替える
- Ctrl-C(および SIGTERM)で止めると、実行中だったファイルは状態ファイルに記録されない。
  ffmpeg も同時に死ぬがそれは「そのファイルの失敗」ではないので、再実行でやり直される
- 各出力先は単一の実行だけで処理する。同じ `--root` や同じ `--retire` を指す実行を同時に
  走らせないこと(並列化はプロセス内だけで、プロセス間の排他はない)
- 対象は CFR、動画 1 ストリーム、音声 0 または 1 ストリーム。VFR や追加コンテンツは処理前にエラーにする

More