Files
vid_convert/docs/superpowers/plans/2026-03-22-av1-opus.md
2026-03-22 23:13:19 +01:00

698 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Migration AV1-SVT + Opus Implementation Plan
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal :** Migrer `vid_convert.py` de H.265/AAC-LC vers AV1-SVT/Opus avec sélection automatique du CRF via `ab-av1 crf-search`, sortie MKV, et support HDR10/HDR10+/Dolby Vision complet.
**Architecture :** Script Python mono-fichier modifié en place. Les fonctions obsolètes (`mkv_to_mp4`, `is_interlaced`) sont supprimées, la nouvelle fonction `find_crf` est ajoutée, et toutes les fonctions existantes sont mises à jour. Le flux principal perd la logique de chunking.
**Tech Stack :** Python 3, FFmpeg (libsvtav1, libopus), ab-av1, hdr10plus_parser, mkvmerge
**Spec :** `docs/superpowers/specs/2026-03-22-av1-opus-design.md`
---
## Chunk 1 : Nettoyage, corrections et audio Opus
### Task 1 : Correction du bug et suppression du code obsolète
**Fichiers :**
- Modifier : `vid_convert.py`
- [ ] **Étape 1 : Décommenter `get_infos` et supprimer la ligne orpheline**
Dans `vid_convert.py`, ligne 308, remplacer :
```python
# infos = get_infos(file)
interlaced = False
interlaced = is_interlaced(file, infos)
```
Par :
```python
infos = get_infos(file)
```
- [ ] **Étape 2 : Supprimer la fonction `is_interlaced`**
Supprimer les lignes 81104 (fonction `is_interlaced` entière).
- [ ] **Étape 3 : Supprimer la fonction `mkv_to_mp4`**
Supprimer les lignes 283290 (fonction `mkv_to_mp4` entière).
- [ ] **Étape 4 : Mettre à jour les arguments CLI**
Dans le bloc `__main__`, remplacer :
```python
parser.add_argument("-t", "--starttime", dest="starttime")
parser.add_argument("-a", "--animation", dest="animation", action="store_true")
parser.add_argument("-c", "--vhs", dest="vhs", action="store_true")
```
Par :
```python
parser.add_argument("-a", "--animation", dest="animation", action="store_true")
parser.add_argument("-c", "--vhs", dest="vhs", action="store_true")
parser.add_argument("--interlaced", dest="interlaced", action="store_true")
```
- [ ] **Étape 5 : Supprimer le flux principal obsolète**
> ⚠️ Après cette étape, le script sera syntaxiquement valide mais non fonctionnel. Il sera complété au Task 8.
Dans le bloc `__main__`, supprimer exactement ce bloc (état du fichier après Step 1, à partir de `file = args.f_input` jusqu'à la fin du fichier) :
```python
file = args.f_input
infos = get_infos(file)
cropsize = cropping(file, infos)
volumes = volume_audio(file, infos)
if args.stab:
stabilization(file)
if args.animation:
animation = True
else:
animation = False
if not args.starttime:
for track in infos['subtitles']:
extract_subs(file, track['index'], track['language'])
for track in infos['audio']:
convert_audio(file, track['index'], volumes[track['index']], track['channels'], track['channel_layout'], track['language'], track['title'])
if args.starttime:
vid_part_time = int(args.starttime)
else:
vid_part_time = 0
while vid_part_time < infos['duration']:
crf = 20
convert_video(file, infos, vid_part_time, cropsize, crf, animation, interlaced, args.vhs)
vid_part_time += 300
create_mkv(file)
mkv_to_mp4(file)
```
Remplacer par un placeholder temporaire (sera remplacé au Task 8) :
```python
pass
```
- [ ] **Étape 6 : Vérifier que le script parse sans erreur**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 7 : Commit**
```bash
git add vid_convert.py
git commit -m "refactor: suppression code obsolète (chunks, is_interlaced, mkv_to_mp4, -t)"
```
---
### Task 2 : Encodage audio — libfdk_aac → libopus
**Fichiers :**
- Modifier : `vid_convert.py` (fonction `convert_audio`)
- [ ] **Étape 1 : Vérifier que libopus est disponible dans FFmpeg**
```bash
ffmpeg -codecs 2>/dev/null | grep -i opus
```
Résultat attendu : une ligne contenant `libopus` et `A..... libopus`. Si absent, libopus doit être compilé dans FFmpeg avant de continuer.
- [ ] **Étape 2 : Remplacer la fonction `convert_audio`**
Remplacer l'intégralité de la fonction `convert_audio` par :
```python
def convert_audio(file, track, volume_adj, channels, channel_layout, language, title):
if channel_layout == "5.1(side)":
channel_layout = "5.1"
if channels <= 2:
bitrate = "128k"
elif channels == 6:
bitrate = "320k"
else:
bitrate = "450k"
codec = f'libopus -vbr on -b:a {bitrate}'
metadatas = f'-metadata language="{language}" -metadata title="{title}"'
command = f'ffmpeg -loglevel error -i {file} -map 0:{track} -map_metadata -1 -vn -sn -c:a {codec} -mapping_family 1 -filter:a volume={volume_adj},aformat=channel_layouts={channel_layout} {metadatas} -y {file}_audio_{track}_{language}.mka'
logging.debug(command)
result = subprocess.getoutput(command)
if result != "":
logging.error(result)
```
- [ ] **Étape 3 : Vérifier le parsing**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 4 : Commit**
```bash
git add vid_convert.py
git commit -m "feat: encodage audio libopus VBR avec bitrate adaptatif"
```
---
## Chunk 2 : Sélection CRF et encodage vidéo AV1
### Task 3 : Nouvelle fonction `find_crf`
**Fichiers :**
- Modifier : `vid_convert.py` (ajout de fonction avant `convert_video`)
- [ ] **Étape 1 : Ajouter la fonction `find_crf` dans `vid_convert.py`**
- [ ] **Étape 1 : Ajouter `import os` dans les imports du module**
En haut du fichier, après `from os import listdir, remove`, ajouter :
```python
import os
```
- [ ] **Étape 2 : Ajouter la fonction `find_crf`**
Insérer après la fonction `volume_audio` et avant `stabilization` :
```python
VMAF_HDR_MODEL = '/usr/share/vmaf/model/vmaf_4k_v0.6.1.json'
def find_crf(file, enc_options, hdr=False):
'''
Détermine le CRF optimal via ab-av1 crf-search pour atteindre VMAF 96.
Pour les sources HDR, utilise un modèle VMAF calibré 4K HDR si disponible.
'''
logging.info("Recherche du CRF optimal (VMAF 96)...")
vmaf_model = ''
if hdr and os.path.exists(VMAF_HDR_MODEL):
vmaf_model = f'--vmaf-model path={VMAF_HDR_MODEL}'
logging.debug(f"Modèle VMAF HDR utilisé : {VMAF_HDR_MODEL}")
cmd = f'ab-av1 crf-search --input {file} --encoder libsvtav1 --vmaf 96 {vmaf_model} --enc {enc_options}'
logging.debug(cmd)
result = subprocess.getoutput(cmd)
logging.debug(result)
for line in result.splitlines():
# ab-av1 émet une ligne du type : "crf 32 VMAF 96.21 ..."
if 'crf' in line and 'VMAF' in line:
try:
crf = int(line.split('crf')[1].split()[0])
logging.info(f"CRF optimal trouvé : {crf}")
return crf
except (IndexError, ValueError):
pass
logging.warning("ab-av1 crf-search a échoué, utilisation du CRF par défaut (32)")
return 32
```
- [ ] **Étape 3 : Vérifier le parsing**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 4 : Commit**
```bash
git add vid_convert.py
git commit -m "feat: ajout find_crf via ab-av1 crf-search (VMAF 96)"
```
---
### Task 4 : Encodage vidéo — libx265 → libsvtav1
**Fichiers :**
- Modifier : `vid_convert.py` (fonction `convert_video`)
- [ ] **Étape 1 : Remplacer la signature et les variables de base**
Remplacer le début de `convert_video` :
```python
def convert_video(file, infos, start, crop, crf, animation, interlaced, vhs):
str_start = "{:05d}".format(start)
output = f'{file}_video_t{str_start}.mkv'
fmt = "yuv420p10le"
track = infos['video']['index']
codec = 'libx265 -preset slower'
hdr = ''
if animation:
tune = "-tune animation"
else:
tune = ""
if interlaced:
crop = f"{crop},yadif"
if vhs:
crop = f"{crop},hqdn3d,unsharp=5:5:0.8:3:3:0.4"
```
Par :
```python
def convert_video(file, infos, crf, crop, enc_options, interlaced, vhs):
output = f'{file}_video.mkv'
fmt = "yuv420p10le"
track = infos['video']['index']
codec = 'libsvtav1'
svtav1_params = enc_options
hdr = ''
if interlaced:
crop = f"{crop},yadif"
if vhs:
crop = f"{crop},hqdn3d,unsharp=5:5:0.8:3:3:0.4"
```
- [ ] **Étape 2 : Mettre à jour le bloc HDR10 statique**
Remplacer le bloc `if 'side_data_list' in infos['video'].keys():` jusqu'à sa fin :
```python
if 'side_data_list' in infos['video'].keys():
try:
light_level = f"{infos['video']['side_data_list'][1]['max_content']},{infos['video']['side_data_list'][1]['max_average']}"
color_primaries = infos['video']['color_primaries']
color_transfer = infos['video']['color_transfer']
color_space = infos['video']['color_space']
green_x = infos['video']['side_data_list'][0]['green_x'].split('/')
green_x = int(int(green_x[0])*(int(green_x[1])/50000))
green_y = infos['video']['side_data_list'][0]['green_y'].split('/')
green_y = int(int(green_y[0])*(int(green_y[1])/50000))
green = f'G\({green_x},{green_y}\)'
blue_x = infos['video']['side_data_list'][0]['blue_x'].split('/')
blue_x = int(int(blue_x[0])*(int(blue_x[1])/50000))
blue_y = infos['video']['side_data_list'][0]['blue_y'].split('/')
blue_y = int(int(blue_y[0])*(int(blue_y[1])/50000))
blue = f'B\({blue_x},{blue_y}\)'
red_x = infos['video']['side_data_list'][0]['red_x'].split('/')
red_x = int(int(red_x[0])*(int(red_x[1])/50000))
red_y = infos['video']['side_data_list'][0]['red_y'].split('/')
red_y = int(int(red_y[0])*(int(red_y[1])/50000))
red = f'R\({red_x},{red_y}\)'
white_point_x = infos['video']['side_data_list'][0]['white_point_x'].split('/')
white_point_x = int(int(white_point_x[0])*(int(white_point_x[1])/50000))
white_point_y = infos['video']['side_data_list'][0]['white_point_y'].split('/')
white_point_y = int(int(white_point_y[0])*(int(white_point_y[1])/50000))
white_point = f'WP\({white_point_x},{white_point_y}\)'
min_luminance = infos['video']['side_data_list'][0]['min_luminance'].split('/')
min_luminance = int(int(min_luminance[0])*(int(min_luminance[1])/10000))
max_luminance = infos['video']['side_data_list'][0]['max_luminance'].split('/')
max_luminance = int(int(max_luminance[0])*(int(max_luminance[1])/10000))
luminance = f'L\({max_luminance},{min_luminance}\)'
master_display = green + blue + red + white_point + luminance
hdr = f'mastering-display={master_display}:content-light={light_level}'
if svtav1_params:
svtav1_params = f'{svtav1_params}:{hdr}'
else:
svtav1_params = hdr
except Exception as err:
logging.debug(f"Aucune information HDR statique : {err}")
```
- [ ] **Étape 3 : Mettre à jour l'injection HDR10+ et la commande FFmpeg finale**
Remplacer la fin de `convert_video` (bloc HDR10+ + commande) :
```python
if 'hdr10plus' in infos['video']:
hdr10plus_param = f"hdr10plus-json={infos['video']['hdr10plus_metadata']}"
if svtav1_params:
svtav1_params = f'{svtav1_params}:{hdr10plus_param}'
else:
svtav1_params = hdr10plus_param
svtav1_args = f'-svtav1-params {svtav1_params}' if svtav1_params else ''
command = f'ffmpeg -loglevel error -i {file} -map 0:{track} -an -sn -c:v {codec} {svtav1_args} -crf {crf} -pix_fmt {fmt} -filter:v {crop} -y {output}'
logging.info("Encodage vidéo en cours (AV1-SVT)...")
logging.debug(command)
result = subprocess.getoutput(command)
if result != "":
logging.error(result)
```
- [ ] **Étape 4 : Vérifier le parsing**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 5 : Commit**
```bash
git add vid_convert.py
git commit -m "feat: encodage vidéo libsvtav1 avec HDR10 statique et HDR10+ via svtav1-params"
```
---
## Chunk 3 : HDR10+, Dolby Vision, MKV et flux principal
### Task 5 : Détection du conteneur source pour HDR10+
**Fichiers :**
- Modifier : `vid_convert.py` (fonction `get_infos`)
- [ ] **Étape 1 : Détecter le format du conteneur dans `get_infos`**
Dans `get_infos`, remplacer le bloc de récupération de la durée ET le bloc d'extraction HDR10+ (les deux appels ffprobe sont fusionnés en un seul) :
Remplacer :
```python
duration = subprocess.getoutput(f"ffprobe -v quiet -print_format json -show_format {file}")
duration = json.loads(duration)
duration = float(duration['format']['duration'])
hdr10_v_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy -vbsf hevc_mp4toannexb -f hevc - | hdr10plus_parser -o metadata.json --verify -'
hdr10_v_raw = subprocess.getoutput(hdr10_v_cmd)
if 'metadata detected' in hdr10_v_raw:
hdr10_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy -vbsf hevc_mp4toannexb -f hevc - | hdr10plus_parser -o /tmp/{file}_hdr10_metadata.json -'
hdr10_cmd_res = subprocess.getoutput(hdr10_cmd)
logging.debug(hdr10_cmd_res)
v_infos.update({'hdr10plus': True, 'hdr10plus_metadata': f'/tmp/{file}_hdr10_metadata.json'})
```
Par :
```python
format_data = json.loads(subprocess.getoutput(f"ffprobe -v quiet -print_format json -show_format {file}"))
duration = float(format_data['format']['duration'])
format_name = format_data.get('format', {}).get('format_name', '')
is_mp4_container = 'mp4' in format_name or 'mov' in format_name
bsf = '-vbsf hevc_mp4toannexb' if is_mp4_container else ''
hdr10_v_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy {bsf} -f hevc - | hdr10plus_parser --verify -'
hdr10_v_raw = subprocess.getoutput(hdr10_v_cmd)
if 'metadata detected' in hdr10_v_raw:
hdr10_cmd = f'ffmpeg -loglevel panic -i {file} -c:v copy {bsf} -f hevc - | hdr10plus_parser -o /tmp/{file}_hdr10_metadata.json -'
hdr10_cmd_res = subprocess.getoutput(hdr10_cmd)
logging.debug(hdr10_cmd_res)
v_infos.update({'hdr10plus': True, 'hdr10plus_metadata': f'/tmp/{file}_hdr10_metadata.json'})
```
- [ ] **Étape 2 : Vérifier le parsing**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 3 : Commit**
```bash
git add vid_convert.py
git commit -m "fix: détection conteneur source pour extraction HDR10+ (MKV vs MP4)"
```
---
### Task 6 : Détection Dolby Vision
**Fichiers :**
- Modifier : `vid_convert.py` (fonction `get_infos`)
- [ ] **Étape 1 : Ajouter la détection DV après le bloc HDR10+**
Dans `get_infos`, insérer le code suivant **au niveau d'indentation de la fonction** (pas à l'intérieur du `if`), après la ligne `v_infos.update({'hdr10plus': True, ...})` et avant la ligne `infos = {'duration': duration, ...}`.
Concrètement, le code s'insère entre ces deux ancres exactes :
```python
v_infos.update({'hdr10plus': True, 'hdr10plus_metadata': f'/tmp/{file}_hdr10_metadata.json'})
# ← fin du bloc if 'metadata detected' (dés-indentation ici)
infos = {'duration': duration, 'video': v_infos, 'audio': a_infos, 'subtitles': s_infos}
```
Insérer (au niveau d'indentation de la fonction, soit 4 espaces) :
```python
try:
for side_data in full_v_infos['frames'][0].get('side_data_list', []):
if side_data.get('side_data_type') == 'DOVI configuration record':
dv_profile = side_data.get('dv_profile', '?')
logging.warning(
f"Dolby Vision détecté (Profile {dv_profile}). "
f"Encodage sans couche DV — HDR10 utilisé si disponible."
)
v_infos.update({'dolby_vision_profile': dv_profile})
break
except (KeyError, IndexError):
pass
```
- [ ] **Étape 2 : Vérifier le parsing**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 3 : Commit**
```bash
git add vid_convert.py
git commit -m "feat: détection Dolby Vision avec avertissement (fallback HDR10)"
```
---
### Task 7 : Simplification de `create_mkv`
**Fichiers :**
- Modifier : `vid_convert.py` (fonction `create_mkv`)
- [ ] **Étape 1 : Réécrire `create_mkv` pour une seule piste vidéo**
Remplacer l'intégralité de la fonction `create_mkv` par :
```python
def create_mkv(filename):
json_data = []
json_data.append("--output")
json_data.append(f"NEW_{filename}.mkv")
video_file = f"{filename}_video.mkv"
json_data += ["--no-track-tags", "--no-global-tags", "--no-chapters",
"(", video_file, ")"]
for file in listdir():
if f"{filename}_audio" in file:
lang = file[-7:][:-4]
json_data += ["--no-track-tags", "--no-global-tags", "--no-chapters",
"--language", f"0:{lang}",
"(", file, ")"]
for file in listdir():
if f"{filename}_subtitle" in file:
lang = file[-7:][:-4]
json_data += ["--no-track-tags", "--no-global-tags", "--no-chapters",
"--language", f"0:{lang}",
"(", file, ")"]
with open(f"/tmp/{filename}.json", "w") as mkvmerge_options:
mkvmerge_options.write(json.dumps(json_data))
command = f"mkvmerge -v @/tmp/{filename}.json"
logging.debug(command)
result = subprocess.getoutput(command)
logging.info(result)
remove(f"/tmp/{filename}.json")
for file in listdir():
if file == video_file:
remove(file)
if f"{filename}_audio" in file:
remove(file)
if f"{filename}_subtitle" in file:
remove(file)
```
- [ ] **Étape 2 : Vérifier le parsing**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 3 : Commit**
```bash
git add vid_convert.py
git commit -m "refactor: create_mkv simplifié — une seule piste vidéo, sortie NEW_{filename}.mkv"
```
---
### Task 8 : Mise à jour du flux principal (`__main__`)
**Fichiers :**
- Modifier : `vid_convert.py` (bloc `__main__`)
- [ ] **Étape 1 : Réécrire le flux principal**
Remplacer tout le contenu du bloc `__main__` à partir de `file = args.f_input` jusqu'à la fin du fichier par :
```python
file = args.f_input
infos = get_infos(file)
if args.stab:
stabilization(file)
cropsize = cropping(file, infos)
volumes = volume_audio(file, infos)
enc_options = 'preset=3:tune=0'
is_hdr = 'side_data_list' in infos['video'] or 'hdr10plus' in infos['video']
crf = find_crf(file, enc_options, hdr=is_hdr)
for track in infos['subtitles']:
extract_subs(file, track['index'], track['language'])
for track in infos['audio']:
convert_audio(file, track['index'], volumes[track['index']], track['channels'],
track['channel_layout'], track['language'], track['title'])
convert_video(file, infos, crf, cropsize, enc_options, args.interlaced, args.vhs)
create_mkv(file)
```
Note : `tune=0` (VQ) est utilisé pour tous les contenus y compris l'animation, conformément à la spec.
- [ ] **Étape 2 : Vérifier le parsing**
```bash
python3 -c "import ast; ast.parse(open('vid_convert.py').read()); print('OK')"
```
Résultat attendu : `OK`
- [ ] **Étape 3 : Vérifier l'aide CLI**
```bash
python3 vid_convert.py --help
```
Résultat attendu : affichage des arguments `f_input`, `-d`, `-s`, `-a`, `-c`, `--interlaced` — sans `-t`.
- [ ] **Étape 4 : Commit**
```bash
git add vid_convert.py
git commit -m "feat: flux principal AV1+Opus — find_crf, encode one-pass, sortie MKV"
```
---
### Task 9 : Mise à jour du README et du CLAUDE.md
**Fichiers :**
- Modifier : `README.md`
- Modifier : `CLAUDE.md`
- [ ] **Étape 1 : Mettre à jour README.md**
Remplacer (noter l'espace de fin en fin de ligne 2 : `...avec un audio en `) :
```
vid_convert est un script qui utilise ffmpeg pour convertir des vidéos au
format [H.265](https://fr.wikipedia.org/wiki/H.265/HEVC), avec un audio en
[AAC-LC](https://fr.wikipedia.org/wiki/Advanced_Audio_Coding).
```
Par :
```
vid_convert est un script qui utilise ffmpeg pour convertir des vidéos au
format [AV1](https://fr.wikipedia.org/wiki/AV1), avec un audio en
[Opus](https://fr.wikipedia.org/wiki/Opus_(codec)).
```
Remplacer :
```
Le format de sortie est un fichier MPEG-4 (.mp4), lisible sur n'importe quel
appareil (ou presque) disposant d'une puissance de décodage suffisante.
```
Par :
```
Le format de sortie est un fichier Matroska (.mkv), optimisé pour l'archivage
de demux DVD et BluRay.
```
Remplacer la section dépendances :
```
- [Python](https://www.python.org/) (>= 3.5)
- [ffmpeg](https://ffmpeg.org/)
- [hdr10plus_tool](https://github.com/quietvoid/hdr10plus_tool)
```
Par :
```
- [Python](https://www.python.org/) (>= 3.5)
- [ffmpeg](https://ffmpeg.org/) (avec libsvtav1 et libopus)
- [ab-av1](https://github.com/alexheretic/ab-av1)
- hdr10plus_parser (binaire inclus dans le dépôt)
```
- [ ] **Étape 2 : Mettre à jour CLAUDE.md — section Projet**
Remplacer :
```
Script Python standalone de conversion vidéo vers H.265/AAC-LC en conteneur MP4, avec support HDR10+, désentrelacement, détection de bandes noires, et normalisation audio.
```
Par :
```
Script Python standalone de conversion vidéo vers AV1/Opus en conteneur MKV, avec support HDR10/HDR10+/Dolby Vision (fallback HDR10), sélection automatique du CRF via ab-av1, détection de bandes noires, et normalisation audio. Cible : archivage de demux DVD/BluRay.
```
- [ ] **Étape 3 : Mettre à jour CLAUDE.md — section Utilisation**
Remplacer :
```
./vid_convert.py -t <hh:mm:ss> <fichier> # reprendre à partir d'une position
```
Par :
```
./vid_convert.py --interlaced <fichier> # forcer le désentrelacement (yadif)
```
- [ ] **Étape 4 : Mettre à jour CLAUDE.md — section Dépendances**
Remplacer :
```
- `ffmpeg` et `ffprobe` — encodage et extraction de métadonnées
- `mkvmerge` (MkvToolNix) — assemblage du conteneur intermédiaire
- `hdr10plus_tool` (binaire inclus : `hdr10plus_parser`) — métadonnées HDR10+
```
Par :
```
- `ffmpeg` et `ffprobe` — encodage et extraction de métadonnées (libsvtav1 et libopus requis)
- `ab-av1` — sélection automatique du CRF par VMAF
- `mkvmerge` (MkvToolNix) — assemblage du conteneur MKV final
- `hdr10plus_parser` (binaire inclus) — extraction des métadonnées HDR10+
```
- [ ] **Étape 5 : Mettre à jour CLAUDE.md — section Architecture**
Remplacer l'intégralité de la section Architecture (de la ligne `Fichier unique` jusqu'à `deux passes`) par :
```
Fichier unique : `vid_convert.py`. Flux d'exécution séquentiel :
1. **`get_infos(file)`** — ffprobe JSON → métadonnées vidéo/audio/sous-titres + détection HDR10+/DV
2. **`cropping(file, infos)`** — cropdetect FFmpeg → suppression des bandes noires
3. **`volume_audio(file, infos)`** — volumedetect FFmpeg → ajustement par piste audio
4. **`find_crf(file, enc_options, hdr)`** — ab-av1 crf-search VMAF 96 → CRF optimal
5. **`extract_subs(file, track, lang)`** — extraction des pistes de sous-titres
6. **`convert_audio(...)`** — encodage libopus VBR par piste
7. **`convert_video(...)`** — encodage libsvtav1 CRF=auto preset=3, passe unique
8. **`create_mkv(filename)`** — mkvmerge → MKV final `NEW_{filename}.mkv`
### Points clés
- Sortie vidéo : `yuv420p10le` (10 bits), HDR10 via `-svtav1-params`, HDR10+ via `hdr10plus-json`
- `find_crf` utilise le modèle VMAF HDR (`vmaf_4k_v0.6.1.json`) si disponible sur le système
- Les fichiers temporaires passent par `/tmp/` (métadonnées HDR10+, options mkvmerge JSON)
- Dolby Vision Profile 8 → fallback HDR10 automatique ; Profile 5 → avertissement
- Le filtre VHS combine `hqdn3d` + `unsharp`
- La stabilisation utilise `vidstabdetect` + `vidstabtransform` en deux passes
```
- [ ] **Étape 6 : Mettre à jour CLAUDE.md — section Gotchas**
Remplacer l'intégralité de la section Gotchas par :
```
## Gotchas
- **libsvtav1 et libopus requis** : le binaire FFmpeg doit être compilé avec `--enable-libsvtav1` et `--enable-libopus`. Vérifier avec `ffmpeg -codecs | grep -E 'svtav1|opus'`.
- **ab-av1 requis** : doit être installé et disponible dans le PATH. Voir https://github.com/alexheretic/ab-av1.
- **Fichiers temporaires orphelins** : en cas d'interruption, `{fichier}_video.mkv`, `{fichier}_audio_*.mka` et fichiers HDR10+ peuvent rester dans le répertoire courant ou `/tmp/`. Les supprimer manuellement si besoin.
- **Fichier de sortie** : `NEW_{nom_source}.mkv` généré dans le même répertoire que la source.
```
- [ ] **Étape 7 : Vérification**
```bash
grep -c "AV1\|Opus\|MKV\|ab-av1" README.md
grep -c "libsvtav1\|libopus\|ab-av1\|find_crf" CLAUDE.md
```
Résultat attendu : chaque commande retourne un entier > 0.
- [ ] **Étape 8 : Commit**
```bash
git add README.md CLAUDE.md
git commit -m "docs: mise à jour README et CLAUDE.md pour AV1+Opus+MKV"
```